Reputation: 13444
I am trying to upload a video to twitter on Android.
Every time I use the API, I get a HTTP 400 (with no error message), on the APPEND command.
My request looks like this (using httpbin)
HEADERS
User-Agent: okhttp/3.2.0
Content-Length: 4000810
Total-Route-Time: 0
Accept-Encoding: gzip
Cf-Ipcountry: JP
Connection: close
Authorization: OAuth oauth_consumer_key="[Redacted]", oauth_nonce="[Redacted]", oauth_signature="[Redacted]%2BkY9kybcE%3D", oauth_signature_method="HMAC-SHA1", oauth_timestamp="1476240797", oauth_token="-[Redacted]", oauth_version="1.0"
Host: requestb.in
Content-Type: multipart/form-data; boundary=81302723-6d1b-4c0b-898a-ca52dd2aef10
Cf-Connecting-Ip: 118.238.220.243
X-Request-Id: e68c732e-5b59-4671-823a-f2ef1aa4e5c7
Via: 1.1 vegur
Cf-Visitor: {"scheme":"http"} => https for the real one
Connect-Time: 0
Cf-Ray: 2f0742ba47e0132f-NRT
RAW BODY
--81302723-6d1b-4c0b-898a-ca52dd2aef10
Content-Disposition: form-data; name="command"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 8
"APPEND"
--81302723-6d1b-4c0b-898a-ca52dd2aef10
Content-Disposition: form-data; name="media_id"
Content-Transfer-Encoding: binary
Content-Type: application/json; charset=UTF-8
Content-Length: 20
"[REDACTED]"
--81302723-6d1b-4c0b-898a-ca52dd2aef10
Content-Disposition: form-data; name="media"
Content-Transfer-Encoding: binary
Content-Type: video/avc
[Redacted binary gibberish]
The error I get while using the API is:
Response { protocol=h2, code=400, message=, url=https://upload.twitter.com/1.1/media/upload.json }
The final file is 6mb; and ffmpeg -i on the file yeild the result
Metadata: major_brand : mp42 minor_version : 0 compatible_brands: isommp42 creation_time : 2016-10-12 02:21:19 com.android.version: 6.0 Duration: 00:00:22.12, start: 0.000000, bitrate: 2387 kb/s Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 480x480, 1135 kb/s, SAR 1:1 DAR 1:1, 25 fps, 25 tbr, 90k tbn, 50 tbc (default) Metadata: creation_time : 2016-10-12 02:21:19 handler_name : VideoHandle
The code to handle the upload looks like this:
class MPTwitterApiClient extends TwitterApiClient {
public MPTwitterApiClient(TwitterSession session) {
super(session);
}
/**
* Provide CustomService with defined endpoints
*/
public VideoService getVideoService() {
return getService(VideoService.class);
}
}
// example users/show service endpoint
interface VideoService {
@FormUrlEncoded()
@POST("https://upload.twitter.com/1.1/media/upload.json")
Call<VideoUploadInit> uploadVideoInit(@Field("command") String command,
@Field("total_bytes") String totalBytes,
@Field("media_type") String mediaType);
@Multipart
@POST("https://upload.twitter.com/1.1/media/upload.json")
Call <VideoUploadPart>uploadVideoAppend(@Part("command") String command,
@Part("media_id") String mediaId,
@Part("media") RequestBody media, // The raw binary file content being uploaded. Cannot be used with media_data.
// Required after an INIT, an index number starting at zero indicating the order of the uploaded chunks.
// The chunk of the upload for a single media, from 0-999, inclusive.
// The first segment_index is 0, the second segment uploaded is 1, etc.
@Part("segment_index") String segmentIndex);
@POST("https://upload.twitter.com/1.1/media/upload.json")
@FormUrlEncoded()
Call<VideoUploadEnd> uploadVideoFinalize(@Field("command") String command,
@Field("media_id") long mediaId);
public class VideoUploadInit {
@SerializedName("media_id")
public final long mediaId;
public VideoUploadInit(final long pMediaId) {
mediaId = pMediaId;
}
}
public class VideoUploadPart {
}
public class VideoUploadEnd {
}
}
And the upload code:
private void uploadChunk(final VideoService videoService, final byte data[], final long mediaId , final int fileSize, final int chunkPart) {
final int maxChunk = 4 * 1000 * 1000;
final int byteSent = chunkPart * maxChunk;
final boolean isLast = byteSent + maxChunk >= fileSize;
RequestBody body = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("video/mp4");
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
sink.write(data, byteSent, isLast ? fileSize - byteSent : maxChunk);
}
};
videoService.uploadVideoAppend("APPEND", String.valueOf(mediaId), body, String.valueOf(chunkPart)).enqueue(new Callback<VideoService.VideoUploadPart>() {
@Override
public void success(final Result result) {
Log.d(TAG, "Uploaded video part");
if (isLast) {
videoService.uploadVideoFinalize("FINALIZE", mediaId).enqueue(new Callback<VideoService.VideoUploadEnd>() {
@Override
public void success(final Result<VideoService.VideoUploadEnd> result) {
Log.e(TAG, "Finalized upload !");
}
@Override
public void failure(final TwitterException exception) {
Log.e(TAG, "Failed upload finalization");
}
});
} else {
uploadChunk(videoService, data, mediaId, fileSize, chunkPart + 1);
}
}
@Override
public void failure(final TwitterException exception) {
Log.e(TAG, "Could not upload video: " + exception);
}
});
}
private void tweet(TwitterSession pTwitterSession) {
TwitterAuthToken authToken = pTwitterSession.getAuthToken();
String token = authToken.token;
String secret = authToken.secret;
MPTwitterApiClient twitterApiClient = new MPTwitterApiClient(pTwitterSession);
final VideoService videoService = twitterApiClient.getVideoService();
final File videoFile = new File(AssetsUtils.getExportMoviePath(getApplicationContext()));
Log.d(TAG, "File Size is " + (videoFile.length() / 1024 / 1024) + "mb");
try {
final byte data[] = Files.toByteArray(videoFile);
videoService.uploadVideoInit("INIT", String.valueOf(videoFile.length()), "video/mp4").enqueue(new Callback<VideoService.VideoUploadInit>() { //XXX refactor this callback hell
@Override
public void success(final Result<VideoService.VideoUploadInit> result) {
Log.d(TAG, "Succeed INIT RESULT: " +result);
Log.d(TAG, "media ID is " + result.data.mediaId);
final long mediaId = result.data.mediaId;
final int fileSize = (int)videoFile.length();
uploadChunk(videoService, data, mediaId, fileSize, 0);
}
@Override
public void failure(final TwitterException exception) {
Log.d(TAG, "Failed twitter init with " + exception);
}
});
} catch (IOException pE) {
pE.printStackTrace();
}
}
The media INIT call run successfully and return a media id. The append part fail on first chunk.
(Yes, it's all white because the content is still under NDA).
Upvotes: 0
Views: 591
Reputation: 13444
Ok, I finally figured out.
The request was not formatted as twitter wanted it. I updated this code (you'd need a bit of refactor tho).
private void uploadChunk(final VideoService videoService, final byte data[], final long pMediaId , final int fileSize, final int chunkPart) {
final int maxChunk = 4 * 1000 * 1000;
final int byteSent = chunkPart * maxChunk;
final boolean isLast = byteSent + maxChunk >= fileSize;
RequestBody body = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse(Encoder.MIME_TYPE);
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
sink.write(data, byteSent, isLast ? fileSize - byteSent : maxChunk);
}
};
final RequestBody mediaIdBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("text/plain");
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
sink.writeString(String.valueOf(pMediaId), Charset.defaultCharset());
}
};
RequestBody appendCommand = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("text/plain");
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
sink.writeString("APPEND", Charset.defaultCharset());
}
};
RequestBody chunkPartBody = new RequestBody() {
@Override
public MediaType contentType() {
return MediaType.parse("text/plain");
}
@Override
public void writeTo(final BufferedSink sink) throws IOException {
sink.writeString(String.valueOf(chunkPart), Charset.defaultCharset());
}
};
videoService.uploadVideoAppend(appendCommand, mediaIdBody, body, chunkPartBody).enqueue(new Callback<VideoService.VideoUploadPart>() {
@Override
public void success(final Result result) {
Log.d(TAG, "Uploaded video part");
if (isLast) {
videoService.uploadVideoFinalize("FINALIZE", pMediaId).enqueue(new Callback<VideoService.VideoUploadEnd>() {
@Override
public void success(final Result<VideoService.VideoUploadEnd> result) {
Log.e(TAG, "Finalized upload !");
}
@Override
public void failure(final TwitterException exception) {
Log.e(TAG, "Failed upload finalization");
}
});
} else {
uploadChunk(videoService, data, pMediaId, fileSize, chunkPart + 1);
}
}
@Override
public void failure(final TwitterException exception) {
Log.e(TAG, "Could not upload video: " + exception);
}
});
}
And in the twitter api client:
@Multipart
@POST("https://upload.twitter.com/1.1/media/upload.json")
Call <VideoUploadPart>uploadVideoAppend(@Part("command") RequestBody command,
@Part("media_id") RequestBody mediaId,
@Part("media") RequestBody media, // The raw binary file content being uploaded. Cannot be used with media_data.
// Required after an INIT, an index number starting at zero indicating the order of the uploaded chunks.
// The chunk of the upload for a single media, from 0-999, inclusive.
// The first segment_index is 0, the second segment uploaded is 1, etc.
@Part("segment_index") RequestBody segmentIndex);
Upvotes: 0