in reference to @Sumit Mahajan 's implementation, twitter has had some updates to their media upload via v2 api.They've now removed the COMMAND= param, and separated the end points for initialize, append and finalize. (i've updated sumit's implementation with the new api endpoints, and used an s3 asset)
https://devcommunity.x.com/t/media-upload-endpoints-update-and-extended-migration-deadline/241818
export const TWITTER_ENDPOINTS = {
TWITTER_TWEET_URL: "https://api.twitter.com/2/tweets",
TWITTER_MEDIA_INITIALIZE: "https://api.twitter.com/2/media/upload/initialize",
TWITTER_MEDIA_APPEND: "https://api.twitter.com/2/media/upload/{id}/append",
TWITTER_MEDIA_FINALIZE: "https://api.twitter.com/2/media/upload/{id}/finalize",
TWITTER_MEDIA_STATUS: "https://api.twitter.com/2/media/upload"
}
const awsMediaResponse = await s3.getObject({
Bucket: bucket,
Key: `path/to/s3_file.mp4`,
}).promise();
if (!awsMediaResponse.Body) throw new Error("No Body returned from s3 object.");
const tokenResponse = await getValidTwitterAccessToken();
const buffer = Buffer.isBuffer(awsMediaResponse.Body) ? awsMediaResponse.Body : Buffer.from(awsMediaResponse.Body as Uint8Array);
const totalBytes = buffer.length;
const mediaUint8 = new Uint8Array(buffer);
const contentType = awsMediaResponse.ContentType;
const CHUNK_SIZE = Math.min(2 * 1024 * 1024, totalBytes);
const initResponse = await fetch(TWITTER_ENDPOINTS.TWITTER_MEDIA_INITIALIZE, {
method: "POST",
headers: {
Authorization: `Bearer ${tokenResponse.twitterAccessToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify({
media_category: "tweet_video",
media_type: contentType,
total_bytes: totalBytes
})
});
if (!initResponse.ok) throw new Error(`Failed to initialize media upload: ${await initResponse.text()}`);
const initData = await initResponse.json();
const mediaId = initData.data.id;
let segmentIndex = 0;
console.log("total: ", totalBytes, "chunk size: ", CHUNK_SIZE);
if (totalBytes <= CHUNK_SIZE) {
const appendFormData = new FormData();
appendFormData.append("media", new Blob([mediaUint8]));
appendFormData.append("segment_index", segmentIndex.toString())
const appendResponse = await fetch(TWITTER_ENDPOINTS.TWITTER_MEDIA_APPEND.replace("{id}", mediaId), {
method: "POST",
headers: {
Authorization: `Bearer ${tokenResponse.twitterAccessToken}`,
"Content-Type": "multipart/form-data"
},
body: appendFormData,
}
);
if (!appendResponse.ok) throw new Error(`Failed to append single chunk media: ${await appendResponse.text()}`)
} else {
for (let byteIndex = 0; byteIndex < totalBytes; byteIndex += CHUNK_SIZE) {
const chunk = mediaUint8.slice(
byteIndex,
Math.min(byteIndex + CHUNK_SIZE, totalBytes)
);
const appendFormData = new FormData();
appendFormData.append("media", new Blob([chunk]));
appendFormData.append("segment_index", segmentIndex.toString())
const appendResponse = await fetch(TWITTER_ENDPOINTS.TWITTER_MEDIA_APPEND.replace("{id}", mediaId), {
method: "POST",
headers: {
Authorization: `Bearer ${tokenResponse.twitterAccessToken}`
},
body: appendFormData,
}
);
if (!appendResponse.ok) throw new Error(`Failed to append media chunk ${segmentIndex}: ${await appendResponse.text()}`);
segmentIndex++;
}
}
const finalizeResponse = await fetch(TWITTER_ENDPOINTS.TWITTER_MEDIA_FINALIZE.replace("{id}", mediaId), {
method: "POST",
headers: {
Authorization: `Bearer ${tokenResponse.twitterAccessToken}`,
},
}
);
if (!finalizeResponse.ok) throw new Error(`Failed to finalize media upload: ${await finalizeResponse.text()}`);
await checkMediaStatus(tokenResponse.twitterAccessToken, mediaId);
console.log("status check: ", mediaId);
const tweetPostResponse = await axios({
url: TWITTER_ENDPOINTS.TWITTER_TWEET_URL,
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${tokenResponse.twitterAccessToken}`
},
data: {
"text": caption,
"media": {
"media_ids": [mediaId]
}
}
})
in addition, the status check function the response has the processing_info in the data object now for twitter:
const statusData = await statusResponse.json();
processingInfo = statusData.data.processing_info;