Skip to content

Attaching media to a Tweet

Roberto Doering edited this page Jun 12, 2020 · 4 revisions

To attach media (images / GIFs / videos) to a Tweet, we first have to upload the media independently and then include the returned mediaId in the statuses/update request (twitterApi.tweetService.update(status: '...')).

Keep in mind

  • This package supports the chunked media upload.
  • A chunk is <= 5 MB in size
  • A tweet may contain up to 4 images, 1 animated GIF or 1 video.
  • Images must be <= 5 MB; GIFs <= 15 MB; videos <= 512 MB

More specifications and recommendations are listed here.

Chunked media upload

  • The upload is initialized using twitterApi.mediaService.uploadInit(...).
  • Each chunk of the media is uploaded using twitterApi.mediaService.uploadAppend(...).
  • After the full media is uploaded, twitterApi.mediaService.uploadFinalize(...) finalizes the upload.
  • After finalizing the upload, it may be necessary to wait for Twitter to process the upload to proceed with the Tweet creation. twitterApi.mediaService.uploadStatus(...) can be used to periodically poll for updates of the media processing operation until a succeeded status is returned and the media can be attached to the Tweet.

Example: Upload from File

In this example, we use package:mime to determine the MIME type of the media file. twitterApi refers to an instance of TwitterApi from this package.

/// Uploads a media file to Twitter.
///
/// Returns the `mediaId` string that can be used when composing a Tweet.
/// Returns `null` if the [media] file is invalid.
///
/// Throws an exception when a request returns an error or times out.
Future<String> upload(File media) async {
  final List<int> mediaBytes = media.readAsBytesSync();
  final int totalBytes = mediaBytes.length;
  final String mediaType = mime(media.path);

  if (totalBytes == 0 || mediaType == null) {
    // unknown type or empty file
    return null;
  }

  // initialize the upload
  final UploadInit uploadInit = await twitterApi.mediaService.uploadInit(
    totalBytes: totalBytes,
    mediaType: mediaType,
  );

  final String mediaId = uploadInit.mediaIdString;

  // `splitList` splits the media bytes into lists with the max length of
  // 500000 (the max chunk size in bytes)
  final List<List<int>> mediaChunks = splitList<int>(
    mediaBytes,
    _maxChunkSize,
  );

  // upload each chunk
  for (int i = 0; i < mediaChunks.length; i++) {
    final List<int> chunk = mediaChunks[i];

    await twitterApi.mediaService.uploadAppend(
      mediaId: mediaId,
      media: chunk,
      segmentIndex: i,
    );
  }

  // finalize the upload
  final UploadFinalize uploadFinalize =
      await twitterApi.mediaService.uploadFinalize(mediaId: mediaId);

  if (uploadFinalize.processingInfo?.pending ?? false) {
    // asynchronous upload of media
    // we have to wait until twitter has processed the upload
    final UploadStatus finishedStatus = await _waitForUploadCompletion(
      mediaId: mediaId,
      sleep: uploadFinalize.processingInfo.checkAfterSecs,
    );

    return finishedStatus?.mediaIdString;
  }

  // media has been uploaded and processed

  return uploadFinalize.mediaIdString;
}

/// Concurrently requests the status of an upload until the uploaded
/// succeeded and waits the suggested time between each call.
///
/// Returns `null` if the upload failed.
Future<UploadStatus> _waitForUploadCompletion({
  @required String mediaId,
  @required int sleep,
}) async {
  await Future.delayed(Duration(seconds: sleep));

  final UploadStatus uploadStatus =
      await twitterApi.mediaService.uploadStatus(mediaId: mediaId);

  if (uploadStatus?.processingInfo?.succeeded == true) {
    // upload processing has succeeded
    return uploadStatus;
  } else if (uploadStatus?.processingInfo?.inProgress == true) {
    // upload is still processing, need to wait longer
    return _waitForUploadCompletion(
      mediaId: mediaId,
      sleep: uploadStatus.processingInfo.checkAfterSecs,
    );
  } else {
    return null;
  }
}

/// Splits the [list] into smaller lists with a max [length].
List<List<T>> splitList<T>(List<T> list, int length) {
  final List<List<T>> chunks = [];
  Iterable<T> chunk;

  do {
    final List<T> remainingEntries = list.sublist(
      chunks.length * length,
    );

    if (remainingEntries.isEmpty) {
      break;
    }

    chunk = remainingEntries.take(length);
    chunks.add(List<T>.from(chunk));
  } while (chunk.length == length);

  return chunks;
}