diff --git a/litr/src/main/java/com/linkedin/android/litr/MediaTransformer.java b/litr/src/main/java/com/linkedin/android/litr/MediaTransformer.java index 169e91f3..a660370d 100644 --- a/litr/src/main/java/com/linkedin/android/litr/MediaTransformer.java +++ b/litr/src/main/java/com/linkedin/android/litr/MediaTransformer.java @@ -57,6 +57,7 @@ public class MediaTransformer { public static final int DEFAULT_KEY_FRAME_INTERVAL = 5; private static final int DEFAULT_AUDIO_BITRATE = 256_000; + private static final int DEFAULT_VIDEO_BITRATE = 10_000_000; private static final int DEFAULT_FRAME_RATE = 30; private static final String TAG = MediaTransformer.class.getSimpleName(); @@ -412,6 +413,10 @@ private MediaFormat createTargetMediaFormat(@NonNull MediaSource mediaSource, sourceMediaFormat.getInteger(MediaFormat.KEY_WIDTH), sourceMediaFormat.getInteger(MediaFormat.KEY_HEIGHT)); int targetBitrate = TranscoderUtils.estimateVideoTrackBitrate(mediaSource, sourceTrackIndex); + if (targetBitrate <= 0) { + // Use a default value in case of failure to extract value from source media + targetBitrate = DEFAULT_VIDEO_BITRATE; + } targetMediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, targetBitrate); int targetKeyFrameInterval = DEFAULT_KEY_FRAME_INTERVAL; diff --git a/litr/src/main/java/com/linkedin/android/litr/io/MediaExtractorMediaSource.java b/litr/src/main/java/com/linkedin/android/litr/io/MediaExtractorMediaSource.java index 689c795b..b6b521e0 100644 --- a/litr/src/main/java/com/linkedin/android/litr/io/MediaExtractorMediaSource.java +++ b/litr/src/main/java/com/linkedin/android/litr/io/MediaExtractorMediaSource.java @@ -31,6 +31,7 @@ public class MediaExtractorMediaSource implements MediaSource { private int orientationHint; private long size; + private final long duration; public MediaExtractorMediaSource(@NonNull Context context, @NonNull Uri uri) throws MediaSourceException { this(context, uri, new MediaRange(0, Long.MAX_VALUE)); @@ -52,6 +53,8 @@ public MediaExtractorMediaSource(@NonNull Context context, @NonNull Uri uri, @No if (rotation != null) { orientationHint = Integer.parseInt(rotation); } + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + duration = (durationStr != null) ? Long.parseLong(durationStr) : -1L; size = TranscoderUtils.getSize(context, uri); // Release unused anymore MediaMetadataRetriever instance releaseQuietly(mediaMetadataRetriever); @@ -124,6 +127,11 @@ public MediaRange getSelection() { return mediaRange; } + @Override + public long getDuration() { + return duration; + } + private void releaseQuietly(MediaMetadataRetriever mediaMetadataRetriever) { try { mediaMetadataRetriever.release(); diff --git a/litr/src/main/java/com/linkedin/android/litr/io/MediaSource.java b/litr/src/main/java/com/linkedin/android/litr/io/MediaSource.java index b6006ac3..8e304722 100644 --- a/litr/src/main/java/com/linkedin/android/litr/io/MediaSource.java +++ b/litr/src/main/java/com/linkedin/android/litr/io/MediaSource.java @@ -99,4 +99,13 @@ public interface MediaSource { default MediaRange getSelection() { return new MediaRange(0, Long.MAX_VALUE); } + + /** + * Returns the playback duration of the media if applicable and known + * + * @return playback duration in milliseconds, -1 if unknown + */ + default long getDuration() { + return -1; + } } diff --git a/litr/src/main/java/com/linkedin/android/litr/utils/TranscoderUtils.java b/litr/src/main/java/com/linkedin/android/litr/utils/TranscoderUtils.java index aaf4c6cd..1b2d3cbb 100644 --- a/litr/src/main/java/com/linkedin/android/litr/utils/TranscoderUtils.java +++ b/litr/src/main/java/com/linkedin/android/litr/utils/TranscoderUtils.java @@ -131,9 +131,9 @@ public static int estimateVideoTrackBitrate(@NonNull MediaSource mediaSource, in if (videoTrackFormat.containsKey(MediaFormat.KEY_BIT_RATE)) { return videoTrackFormat.getInteger(MediaFormat.KEY_BIT_RATE); } - float videoTrackDuration = TimeUtils.microsToSeconds(videoTrackFormat.getLong(MediaFormat.KEY_DURATION)); - if (videoTrackDuration == 0) { - return 0; + float videoTrackDuration = estimateVideoTrackDuration(mediaSource, videoTrackFormat); + if (videoTrackDuration <= 0) { + return -1; } float unallocatedSize = mediaSource.getSize(); @@ -149,9 +149,12 @@ public static int estimateVideoTrackBitrate(@NonNull MediaSource mediaSource, in } else { String mimeType = trackFormat.getString(MediaFormat.KEY_MIME); if (mimeType.startsWith("video")) { - totalPixels += trackFormat.getInteger(MediaFormat.KEY_WIDTH) - * trackFormat.getInteger(MediaFormat.KEY_HEIGHT) - * TimeUtils.microsToSeconds(trackFormat.getLong(MediaFormat.KEY_DURATION)); + float trackDuration = estimateVideoTrackDuration(mediaSource, trackFormat); + if (trackDuration > 0) { + totalPixels += trackFormat.getInteger(MediaFormat.KEY_WIDTH) + * trackFormat.getInteger(MediaFormat.KEY_HEIGHT) + * trackDuration; + } } } } @@ -165,6 +168,23 @@ public static int estimateVideoTrackBitrate(@NonNull MediaSource mediaSource, in return (int) (trackSize * BITS_IN_BYTE / videoTrackDuration); } + /** + * Returns the duration of the given video track. If the given track does not contain a valid + * duration value in its meta data, then the duration value associated with the media source + * shall be returned. + * + * @param mediaSource {@link MediaSource} which contains the video track + * @param videoTrackFormat MediaFormat of the track whose duration needs to be determined + * + * @return Duration of the given video track in seconds. + */ + @VisibleForTesting + static float estimateVideoTrackDuration(MediaSource mediaSource, MediaFormat videoTrackFormat) { + return videoTrackFormat.containsKey(MediaFormat.KEY_DURATION) ? + TimeUtils.microsToSeconds(videoTrackFormat.getLong(MediaFormat.KEY_DURATION)) : + TimeUtils.millisToSeconds(mediaSource.getDuration()); + } + /** * Get size of data abstracted by uri * @param context context to access uri diff --git a/litr/src/test/java/com/linkedin/android/litr/utils/TranscoderUtilsShould.java b/litr/src/test/java/com/linkedin/android/litr/utils/TranscoderUtilsShould.java index 732a4d01..c32da214 100644 --- a/litr/src/test/java/com/linkedin/android/litr/utils/TranscoderUtilsShould.java +++ b/litr/src/test/java/com/linkedin/android/litr/utils/TranscoderUtilsShould.java @@ -172,6 +172,34 @@ public void useVideoTrackBitrateAsEstimationWhenPresent() { assertThat(estimatedBitrate, is(VIDEO_BIT_RATE)); } + @Test + public void useMediaDurationForVideoTrackBitrateEstimationWhenTrackMetadataDoesNotExist() { + long mediaDuration = 50000L; int bitrateUsingMediaDuration = 11400000; + when(mediaSource.getTrackFormat(0)).thenReturn(videoMediaFormat); + when(mediaSource.getDuration()).thenReturn(mediaDuration); + when(videoMediaFormat.containsKey(MediaFormat.KEY_BIT_RATE)).thenReturn(false); + when(videoMediaFormat.containsKey(MediaFormat.KEY_DURATION)).thenReturn(false); + + when(mediaSource.getSize()).thenReturn(VIDEO_BIT_RATE * DURATION_S / 8); + when(mediaSource.getTrackCount()).thenReturn(1); + + int estimatedBitrate = TranscoderUtils.estimateVideoTrackBitrate(mediaSource, 0); + + assertThat(estimatedBitrate, is(bitrateUsingMediaDuration)); + } + + @Test + public void returnInvalidBitrateWhenNeitherTrackDurationNorMediaDurationIsAvailable() { + int invalidBitrate = -1; + when(mediaSource.getTrackFormat(0)).thenReturn(videoMediaFormat); + when(videoMediaFormat.containsKey(MediaFormat.KEY_BIT_RATE)).thenReturn(false); + when(videoMediaFormat.containsKey(MediaFormat.KEY_DURATION)).thenReturn(false); + when(mediaSource.getDuration()).thenReturn((long) invalidBitrate); + + int estimatedBitrate = TranscoderUtils.estimateVideoTrackBitrate(mediaSource, 0); + assertThat(estimatedBitrate, is(invalidBitrate)); + } + @Test public void estimateVideoTrackBitrateWhenSingleVideoTrack() { when(mediaSource.getSize()).thenReturn(VIDEO_BIT_RATE * DURATION_S / 8); @@ -250,6 +278,20 @@ public void estimateVideoTrackBitrateWhenMultipleVideoTracks() { assertEquals(estimatedBitrate, VIDEO_BIT_RATE, 1); } + @Test + public void estimateVideoTrackDurationFromMetadata() { + float durationSecs = TranscoderUtils.estimateVideoTrackDuration(mediaSource, videoMediaFormat); + assertThat(durationSecs, is((float) DURATION_S)); + } + + @Test + public void useMediaDurationAsFallbackWhenEstimatingVideoTrackDuration() { + long mediaDuration = 50_000L; + when(mediaSource.getDuration()).thenReturn(mediaDuration); + when(videoMediaFormat.containsKey(MediaFormat.KEY_DURATION)).thenReturn(false); + float durationSecs = TranscoderUtils.estimateVideoTrackDuration(mediaSource, videoMediaFormat); + assertThat(durationSecs, is((float) mediaDuration/1000)); + } @Test public void estimateWhenTrimmedFromBeginningToMiddle() {