From db1a1dc40cea4d3fc6a3bc239488e9e00bd9adff Mon Sep 17 00:00:00 2001 From: Haruki Hasegawa Date: Sun, 27 Mar 2022 00:10:16 +0900 Subject: [PATCH 001/116] Add more test cases for checking the MediaController#release() behavior Current implementation of the release() method have a bug; it does not clear pending messages/callbacks queued to the applicationHandler. --- .../media3/session/MediaControllerTest.java | 38 +++++++++++++- .../session/MediaControllerTestRule.java | 51 +++++++++++++++++-- ...aControllerWithMediaSessionCompatTest.java | 17 +++++++ 3 files changed, 100 insertions(+), 6 deletions(-) diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java index 4fc4b33a6d..1088be232f 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTest.java @@ -192,7 +192,7 @@ public class MediaControllerTest { } @Test - public void isConnected_afterDisconnection_returnsFalse() throws Exception { + public void isConnected_afterDisconnectionBySessionRelease_returnsFalse() throws Exception { CountDownLatch disconnectedLatch = new CountDownLatch(1); MediaController controller = controllerTestRule.createController( @@ -210,6 +210,42 @@ public class MediaControllerTest { assertThat(controller.isConnected()).isFalse(); } + @Test + public void isConnected_afterDisconnectionByControllerRelease_returnsFalse() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = + controllerTestRule.createController( + remoteSession.getToken(), + null, + new MediaController.Listener() { + @Override + public void onDisconnected(MediaController controller) { + latch.countDown(); + } + }); + threadTestRule.getHandler().postAndSync(controller::release); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controller.isConnected()).isFalse(); + } + + @Test + public void isConnected_afterDisconnectionByControllerReleaseRightAfterCreated_returnsFalse() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = + controllerTestRule.createController( + remoteSession.getToken(), + null, + new MediaController.Listener() { + @Override + public void onDisconnected(MediaController controller) { + latch.countDown(); + } + }, + MediaController::release); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controller.isConnected()).isFalse(); + } + @Test public void close_twice() throws Exception { MediaController controller = controllerTestRule.createController(remoteSession.getToken()); diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java index d629083b15..f1d0b7a758 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerTestRule.java @@ -22,13 +22,17 @@ import static java.util.concurrent.TimeUnit.MILLISECONDS; import android.content.Context; import android.os.Bundle; import android.support.v4.media.session.MediaSessionCompat; +import androidx.annotation.MainThread; import androidx.annotation.Nullable; import androidx.collection.ArrayMap; import androidx.media3.common.util.Log; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.test.core.app.ApplicationProvider; import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; import org.junit.rules.ExternalResource; /** @@ -44,6 +48,15 @@ public final class MediaControllerTestRule extends ExternalResource { private volatile Class controllerType; private volatile long timeoutMs; + public interface MediaControllerCreateListener { + /** + * This callback is invoked immediately after the {@link MediaController} instance is created. + * @param controller {@link MediaController} instance + */ + @MainThread + void onCreated(MediaController controller); + } + public MediaControllerTestRule(HandlerThreadTestRule handlerThreadTestRule) { this.handlerThreadTestRule = handlerThreadTestRule; controllers = new ArrayMap<>(); @@ -96,17 +109,24 @@ public final class MediaControllerTestRule extends ExternalResource { public MediaController createController( MediaSessionCompat.Token token, @Nullable MediaController.Listener listener) throws Exception { + return createController(token, listener, null); + } + + /** Creates {@link MediaController} from {@link MediaSessionCompat.Token}. */ + public MediaController createController( + MediaSessionCompat.Token token, @Nullable MediaController.Listener listener, @Nullable MediaControllerCreateListener controllerCreateListener) + throws Exception { TestMediaBrowserListener testListener = new TestMediaBrowserListener(listener); - MediaController controller = createControllerOnHandler(token, testListener); + MediaController controller = createControllerOnHandler(token, testListener, controllerCreateListener); controllers.put(controller, testListener); return controller; } private MediaController createControllerOnHandler( - MediaSessionCompat.Token token, TestMediaBrowserListener listener) throws Exception { + MediaSessionCompat.Token token, TestMediaBrowserListener listener, @Nullable MediaControllerCreateListener controllerCreateListener) throws Exception { SessionToken sessionToken = SessionToken.createSessionToken(context, token).get(TIMEOUT_MS, MILLISECONDS); - return createControllerOnHandler(sessionToken, /* connectionHints= */ null, listener); + return createControllerOnHandler(sessionToken, /* connectionHints= */ null, listener, controllerCreateListener); } /** Creates {@link MediaController} from {@link SessionToken} with default options. */ @@ -120,14 +140,24 @@ public final class MediaControllerTestRule extends ExternalResource { @Nullable Bundle connectionHints, @Nullable MediaController.Listener listener) throws Exception { + return createController(token, connectionHints, listener, null); + } + + /** Creates {@link MediaController} from {@link SessionToken}. */ + public MediaController createController( + SessionToken token, + @Nullable Bundle connectionHints, + @Nullable MediaController.Listener listener, + @Nullable MediaControllerCreateListener controllerCreateListener) + throws Exception { TestMediaBrowserListener testListener = new TestMediaBrowserListener(listener); - MediaController controller = createControllerOnHandler(token, connectionHints, testListener); + MediaController controller = createControllerOnHandler(token, connectionHints, testListener, controllerCreateListener); controllers.put(controller, testListener); return controller; } private MediaController createControllerOnHandler( - SessionToken token, @Nullable Bundle connectionHints, TestMediaBrowserListener listener) + SessionToken token, @Nullable Bundle connectionHints, TestMediaBrowserListener listener, @Nullable MediaControllerCreateListener controllerCreateListener) throws Exception { // Create controller on the test handler, for changing MediaBrowserCompat's Handler // Looper. Otherwise, MediaBrowserCompat will post all the commands to the handler @@ -153,6 +183,17 @@ public final class MediaControllerTestRule extends ExternalResource { return builder.buildAsync(); } }); + + if (controllerCreateListener != null) { + future.addListener(() -> { + try { + MediaController controller = future.get(); + controllerCreateListener.onCreated(controller); + } catch (ExecutionException ignored) { + } catch (InterruptedException ignored) { + } + }, MoreExecutors.directExecutor()); + } return future.get(timeoutMs, MILLISECONDS); } diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java index 886091efe2..066c4b4a3d 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaControllerWithMediaSessionCompatTest.java @@ -170,6 +170,23 @@ public class MediaControllerWithMediaSessionCompatTest { assertThat(controller.isConnected()).isFalse(); } + @Test + public void disconnected_byControllerReleaseRightAfterCreated() throws Exception { + CountDownLatch latch = new CountDownLatch(1); + MediaController controller = + controllerTestRule.createController( + session.getSessionToken(), + new MediaController.Listener() { + @Override + public void onDisconnected(MediaController controller) { + latch.countDown(); + } + }, + MediaController::release); + assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(controller.isConnected()).isFalse(); + } + @Test public void close_twice_doesNotCrash() throws Exception { MediaController controller = controllerTestRule.createController(session.getSessionToken()); From 7536a25bf5c8637bdcd710161022fb1468774446 Mon Sep 17 00:00:00 2001 From: Haruki Hasegawa Date: Sun, 27 Mar 2022 00:19:57 +0900 Subject: [PATCH 002/116] Call removeCallbacksAndMessages() in MediaController#release() This fixes the NPE ocuured in the MediaControllerImplLegacy#connectToSession() right after MediaController#release() is called. --- .../src/main/java/androidx/media3/session/MediaController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaController.java b/libraries/session/src/main/java/androidx/media3/session/MediaController.java index 53d7b912c1..ac0c416845 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaController.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaController.java @@ -417,6 +417,7 @@ public class MediaController implements Player { return; } released = true; + applicationHandler.removeCallbacksAndMessages(null); try { impl.release(); } catch (Exception e) { From 3b9519c398219063295a0f065cf86aace7a142c5 Mon Sep 17 00:00:00 2001 From: Rakesh Kumar Date: Tue, 8 Mar 2022 10:21:24 +0530 Subject: [PATCH 003/116] Add support for RTSP VP9 Change-Id: Id658564495af13c35fa78ecde9ab587557aabb47 --- .../exoplayer/rtsp/RtpPayloadFormat.java | 4 + .../media3/exoplayer/rtsp/RtspMediaTrack.java | 8 + .../DefaultRtpPayloadReaderFactory.java | 2 + .../exoplayer/rtsp/reader/RtpVP9Reader.java | 230 ++++++++++++++++++ 4 files changed, 244 insertions(+) create mode 100644 libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVP9Reader.java diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java index 297353167b..e96006ea71 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtpPayloadFormat.java @@ -40,6 +40,7 @@ public final class RtpPayloadFormat { private static final String RTP_MEDIA_MPEG4_GENERIC = "MPEG4-GENERIC"; private static final String RTP_MEDIA_H264 = "H264"; private static final String RTP_MEDIA_H265 = "H265"; + private static final String RTP_MEDIA_VP9 = "VP9"; /** Returns whether the format of a {@link MediaDescription} is supported. */ public static boolean isFormatSupported(MediaDescription mediaDescription) { @@ -48,6 +49,7 @@ public final class RtpPayloadFormat { case RTP_MEDIA_H264: case RTP_MEDIA_H265: case RTP_MEDIA_MPEG4_GENERIC: + case RTP_MEDIA_VP9: return true; default: return false; @@ -71,6 +73,8 @@ public final class RtpPayloadFormat { return MimeTypes.VIDEO_H265; case RTP_MEDIA_MPEG4_GENERIC: return MimeTypes.AUDIO_AAC; + case RTP_MEDIA_VP9: + return MimeTypes.VIDEO_VP9; default: throw new IllegalArgumentException(mediaType); } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java index 7547f1ea18..f8aa5c2d8d 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspMediaTrack.java @@ -56,6 +56,10 @@ import com.google.common.collect.ImmutableMap; private static final String GENERIC_CONTROL_ATTR = "*"; + /** Default width and height for VP9. */ + private static final int DEFAULT_VP9_WIDTH = 320; + private static final int DEFAULT_VP9_HEIGHT = 240; + /** The track's associated {@link RtpPayloadFormat}. */ public final RtpPayloadFormat payloadFormat; /** The track's URI. */ @@ -129,6 +133,10 @@ import com.google.common.collect.ImmutableMap; checkArgument(!fmtpParameters.isEmpty()); processH265FmtpAttribute(formatBuilder, fmtpParameters); break; + case MimeTypes.VIDEO_VP9: + // VP9 does not require a FMTP attribute. So Setting default width and height. + formatBuilder.setWidth(DEFAULT_VP9_WIDTH).setHeight(DEFAULT_VP9_HEIGHT); + break; case MimeTypes.AUDIO_AC3: // AC3 does not require a FMTP attribute. Fall through. default: diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java index 888939b7e8..277e717f02 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/DefaultRtpPayloadReaderFactory.java @@ -40,6 +40,8 @@ import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; return new RtpH264Reader(payloadFormat); case MimeTypes.VIDEO_H265: return new RtpH265Reader(payloadFormat); + case MimeTypes.VIDEO_VP9: + return new RtpVP9Reader(payloadFormat); default: // No supported reader, returning null. } diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVP9Reader.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVP9Reader.java new file mode 100644 index 0000000000..3280c0b413 --- /dev/null +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/reader/RtpVP9Reader.java @@ -0,0 +1,230 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.exoplayer.rtsp.reader; + +import static androidx.media3.common.util.Assertions.checkArgument; +import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.common.util.Util.castNonNull; + +import androidx.media3.common.C; +import androidx.media3.common.ParserException; +import androidx.media3.common.util.Log; +import androidx.media3.common.util.ParsableByteArray; +import androidx.media3.common.util.Util; +import androidx.media3.exoplayer.rtsp.RtpPacket; +import androidx.media3.exoplayer.rtsp.RtpPayloadFormat; +import androidx.media3.extractor.ExtractorOutput; +import androidx.media3.extractor.TrackOutput; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; + +/** + * Parses an VP9 byte stream carried on RTP packets, and extracts VP9 Access Units. Refer to + * @link https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9 for more details. + */ +/* package */ final class RtpVP9Reader implements RtpPayloadReader { + + private static final String TAG = "RtpVP9Reader"; + + private static final long MEDIA_CLOCK_FREQUENCY = 90_000; + + private final RtpPayloadFormat payloadFormat; + + private @MonotonicNonNull TrackOutput trackOutput; + @C.BufferFlags private int bufferFlags; + + private long firstReceivedTimestamp; + private long startTimeOffsetUs; + private static int previousSequenceNumber; + /** The combined size of a sample that is fragmented into multiple RTP packets. */ + private int fragmentedSampleSizeBytes; + private static int width; + private static int height; + private static boolean gotFirstPacketOfVP9Frame; + private boolean isKeyFrame; + private boolean isOutputFormatSet; + + /** Creates an instance. */ + public RtpVP9Reader(RtpPayloadFormat payloadFormat) { + this.payloadFormat = payloadFormat; + firstReceivedTimestamp = C.TIME_UNSET; + startTimeOffsetUs = 0; + previousSequenceNumber = C.INDEX_UNSET; + fragmentedSampleSizeBytes = 0; + width = C.INDEX_UNSET; + height = C.INDEX_UNSET; + gotFirstPacketOfVP9Frame = false; + isKeyFrame = false; + isOutputFormatSet = false; + } + + @Override + public void createTracks(ExtractorOutput extractorOutput, int trackId) { + trackOutput = extractorOutput.track(trackId, C.TRACK_TYPE_VIDEO); + castNonNull(trackOutput).format(payloadFormat.format); + } + + @Override + public void onReceivingFirstPacket(long timestamp, int sequenceNumber) {} + + @Override + public void consume(ParsableByteArray data, long timestamp, int sequenceNumber, boolean rtpMarker) + throws ParserException { + checkStateNotNull(trackOutput); + + if (parseVP9Descriptor(data, sequenceNumber)) { + if (fragmentedSampleSizeBytes == 0 && gotFirstPacketOfVP9Frame) { + isKeyFrame = (data.peekUnsignedByte() & 0x04) == 0; + } + + if (!isOutputFormatSet && width > 0 && height > 0) { + if (width != payloadFormat.format.width || height != payloadFormat.format.height) { + trackOutput.format( + payloadFormat.format.buildUpon().setWidth(width).setHeight(height).build()); + } + isOutputFormatSet = true; + } + + int fragmentSize = data.bytesLeft(); + // Write the video sample. + trackOutput.sampleData(data, fragmentSize); + fragmentedSampleSizeBytes += fragmentSize; + + if (rtpMarker) { + if (firstReceivedTimestamp == C.TIME_UNSET) { + firstReceivedTimestamp = timestamp; + } + bufferFlags = isKeyFrame ? C.BUFFER_FLAG_KEY_FRAME : 0; + long timeUs = toSampleUs(startTimeOffsetUs, timestamp, firstReceivedTimestamp); + trackOutput.sampleMetadata( + timeUs, + bufferFlags, + fragmentedSampleSizeBytes, + /* offset= */ 0, + /* encryptionData= */ null); + fragmentedSampleSizeBytes = 0; + gotFirstPacketOfVP9Frame = false; + } + previousSequenceNumber = sequenceNumber; + } + } + + @Override + public void seek(long nextRtpTimestamp, long timeUs) { + firstReceivedTimestamp = nextRtpTimestamp; + fragmentedSampleSizeBytes = 0; + startTimeOffsetUs = timeUs; + } + + // Internal methods. + private static long toSampleUs( + long startTimeOffsetUs, long rtpTimestamp, long firstReceivedRtpTimestamp) { + return startTimeOffsetUs + + Util.scaleLargeTimestamp( + (rtpTimestamp - firstReceivedRtpTimestamp), + /* multiplier= */ C.MICROS_PER_SECOND, + /* divisor= */ MEDIA_CLOCK_FREQUENCY); + } + + private static boolean parseVP9Descriptor(ParsableByteArray payload, int packetSequenceNumber) + throws ParserException { + // VP9 Payload Descriptor, Section 4.2 + // 0 1 2 3 4 5 6 7 + // +-+-+-+-+-+-+-+-+ + // |I|P|L|F|B|E|V|Z| (REQUIRED) + // +-+-+-+-+-+-+-+-+ + // I: |M| PICTURE ID | (RECOMMENDED) + // +-+-+-+-+-+-+-+-+ + // M: | EXTENDED PID | (RECOMMENDED) + // +-+-+-+-+-+-+-+-+ + // L: | TID |U| SID |D| (Conditionally RECOMMENDED) + // +-+-+-+-+-+-+-+-+ + // | TL0PICIDX | (Conditionally REQUIRED) + // +-+-+-+-+-+-+-+-+ + // V: | SS | + // | .. | + // +-+-+-+-+-+-+-+-+ + + int header = payload.readUnsignedByte(); + if (!gotFirstPacketOfVP9Frame) { + // For start of VP9 partition B=1 as per VP9 RFC Section 4.2. + if ((header & 0x08) == 0) { + Log.w( + TAG, + Util.formatInvariant( + "First payload octet of the RTP packet is not the beginning of a new VP9 partition," + + " Dropping current packet." + header)); + return false; + } + gotFirstPacketOfVP9Frame = true; + } else { + // Check that this packet is in the sequence of the previous packet. + int expectedSequenceNumber = RtpPacket.getNextSequenceNumber(previousSequenceNumber); + if (packetSequenceNumber != expectedSequenceNumber) { + Log.w( + TAG, + Util.formatInvariant( + "Received RTP packet with unexpected sequence number. Expected: %d; received: %d." + + " Dropping packet.", + expectedSequenceNumber, packetSequenceNumber)); + return false; + } + } + + // Check I optional header present. + if ((header & 0x80) != 0) { + int optionalHeader = payload.readUnsignedByte(); + // Check M for 15 bits PictureID. + if ((optionalHeader & 0x80) != 0) { + if (payload.bytesLeft() < 1) { + return false; + } + } + } + + // Flexible-mode not implemented. + checkArgument((header & 0x10) == 0, "VP9 flexible mode unsupported"); + + // Check L optional header present. + if ((header & 0x20) != 0) { + payload.skipBytes(1); + if (payload.bytesLeft() < 1) { + return false; + } + // Check TL0PICIDX header present (non-flexible mode). + if ((header & 0x10) == 0) { + payload.skipBytes(1); + } + } + + // Check V optional header present, Refer Section 4.2.1. + if ((header & 0x02) != 0) { + int scalabilityStr = payload.readUnsignedByte(); + int numSpatialLayers = (scalabilityStr & 0xe0) >> 5; + int scalabilityStrLength = ((scalabilityStr & 0x10) != 0) ? numSpatialLayers + 1 : 0; + + if ((scalabilityStr & 0x10) != 0) { + if (payload.bytesLeft() < scalabilityStrLength * 4) { + return false; + } + for (int index = 0; index < scalabilityStrLength; index++) { + width = payload.readUnsignedShort(); + height = payload.readUnsignedShort(); + } + } + } + return true; + } +} From 67acfc67de92d15f3945af9c1b18125187ce0a1c Mon Sep 17 00:00:00 2001 From: aquilescanta Date: Fri, 25 Mar 2022 15:00:57 +0000 Subject: [PATCH 004/116] Support seeking in un-intearleaved tracks in Mp4Extractor A client can pass the id of the track on which they want to seek. PiperOrigin-RevId: 437248055 --- .../media3/extractor/mp4/Mp4Extractor.java | 38 ++++++++++++++----- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java index f56a7dd7c0..4d9c5ec4cc 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/Mp4Extractor.java @@ -281,6 +281,22 @@ public final class Mp4Extractor implements Extractor, SeekMap { @Override public SeekPoints getSeekPoints(long timeUs) { + return getSeekPoints(timeUs, /* trackId= */ C.INDEX_UNSET); + } + + // Non-inherited public methods. + + /** + * Equivalent to {@link SeekMap#getSeekPoints(long)}, except it adds the {@code trackId} + * parameter. + * + * @param timeUs A seek time in microseconds. + * @param trackId The id of the track on which to seek for {@link SeekPoints}. May be {@link + * C#INDEX_UNSET} if the extractor is expected to define the strategy for generating {@link + * SeekPoints}. + * @return The corresponding seek points. + */ + public SeekPoints getSeekPoints(long timeUs, int trackId) { if (tracks.length == 0) { return new SeekPoints(SeekPoint.START); } @@ -290,9 +306,11 @@ public final class Mp4Extractor implements Extractor, SeekMap { long secondTimeUs = C.TIME_UNSET; long secondOffset = C.POSITION_UNSET; + // Note that the id matches the index in tracks. + int mainTrackIndex = trackId != C.INDEX_UNSET ? trackId : firstVideoTrackIndex; // If we have a video track, use it to establish one or two seek points. - if (firstVideoTrackIndex != C.INDEX_UNSET) { - TrackSampleTable sampleTable = tracks[firstVideoTrackIndex].sampleTable; + if (mainTrackIndex != C.INDEX_UNSET) { + TrackSampleTable sampleTable = tracks[mainTrackIndex].sampleTable; int sampleIndex = getSynchronizationSampleIndex(sampleTable, timeUs); if (sampleIndex == C.INDEX_UNSET) { return new SeekPoints(SeekPoint.START); @@ -312,13 +330,15 @@ public final class Mp4Extractor implements Extractor, SeekMap { firstOffset = Long.MAX_VALUE; } - // Take into account other tracks. - for (int i = 0; i < tracks.length; i++) { - if (i != firstVideoTrackIndex) { - TrackSampleTable sampleTable = tracks[i].sampleTable; - firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); - if (secondTimeUs != C.TIME_UNSET) { - secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + if (trackId == C.INDEX_UNSET) { + // Take into account other tracks, but only if the caller has not specified a trackId. + for (int i = 0; i < tracks.length; i++) { + if (i != firstVideoTrackIndex) { + TrackSampleTable sampleTable = tracks[i].sampleTable; + firstOffset = maybeAdjustSeekOffset(sampleTable, firstTimeUs, firstOffset); + if (secondTimeUs != C.TIME_UNSET) { + secondOffset = maybeAdjustSeekOffset(sampleTable, secondTimeUs, secondOffset); + } } } } From 92eb09fa6f4c6d94d866d934f6e9f9eece491954 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 25 Mar 2022 15:44:18 +0000 Subject: [PATCH 005/116] Fix output viewport size of empty FrameProcessorChain. Since the output size can be overridden, the viewport should be ouputWidth/Height and NOT the ExternalCopyFrameProcessor's output size which matches the input size. PiperOrigin-RevId: 437256635 --- .../media3/transformer/FrameProcessorChain.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index fd96d9c5a6..687dbeff19 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -398,18 +398,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private void processFrame() { checkState(Thread.currentThread().equals(glThread)); - Size outputSize = inputSizes.get(0); if (frameProcessors.isEmpty()) { - GlUtil.focusEglSurface( - eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); + GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); } else { GlUtil.focusFramebuffer( eglDisplay, eglContext, eglSurface, framebuffers[0], - outputSize.getWidth(), - outputSize.getHeight()); + inputSizes.get(0).getWidth(), + inputSizes.get(0).getHeight()); } inputSurfaceTexture.updateTexImage(); inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); @@ -418,7 +416,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeNs); for (int i = 0; i < frameProcessors.size() - 1; i++) { - outputSize = inputSizes.get(i + 1); + Size outputSize = inputSizes.get(i + 1); GlUtil.focusFramebuffer( eglDisplay, eglContext, From a5330d43d4c3b5c464d9d35d39c0185216587c90 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 25 Mar 2022 15:44:52 +0000 Subject: [PATCH 006/116] Opt uncontroversial bits of the main demo app into unstable APIs None of these components/features are planned to be part of the initial stable API, so these suppressions will need to be in place when we enable the warnings. PiperOrigin-RevId: 437256731 --- .../androidx/media3/demo/main/DemoDownloadService.java | 2 ++ .../src/main/java/androidx/media3/demo/main/DemoUtil.java | 7 +++++++ .../java/androidx/media3/demo/main/DownloadTracker.java | 2 ++ .../androidx/media3/demo/main/SampleChooserActivity.java | 3 +++ 4 files changed, 14 insertions(+) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java index 21078d8545..c14bfc4ba9 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoDownloadService.java @@ -20,6 +20,7 @@ import static androidx.media3.demo.main.DemoUtil.DOWNLOAD_NOTIFICATION_CHANNEL_I import android.app.Notification; import android.content.Context; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.util.NotificationUtil; import androidx.media3.common.util.Util; import androidx.media3.exoplayer.offline.Download; @@ -32,6 +33,7 @@ import androidx.media3.exoplayer.scheduler.Scheduler; import java.util.List; /** A service for downloading media. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DemoDownloadService extends DownloadService { private static final int JOB_ID = 1; diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java index 2c31ae19ee..2a5a4bfbbe 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DemoUtil.java @@ -16,6 +16,7 @@ package androidx.media3.demo.main; import android.content.Context; +import androidx.annotation.OptIn; import androidx.media3.database.DatabaseProvider; import androidx.media3.database.StandaloneDatabaseProvider; import androidx.media3.datasource.DataSource; @@ -71,6 +72,7 @@ public final class DemoUtil { return BuildConfig.USE_DECODER_EXTENSIONS; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static RenderersFactory buildRenderersFactory( Context context, boolean preferExtensionRenderer) { @DefaultRenderersFactory.ExtensionRendererMode @@ -116,6 +118,7 @@ public final class DemoUtil { return dataSourceFactory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public static synchronized DownloadNotificationHelper getDownloadNotificationHelper( Context context) { if (downloadNotificationHelper == null) { @@ -135,6 +138,7 @@ public final class DemoUtil { return downloadTracker; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized Cache getDownloadCache(Context context) { if (downloadCache == null) { File downloadContentDirectory = @@ -146,6 +150,7 @@ public final class DemoUtil { return downloadCache; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized void ensureDownloadManagerInitialized(Context context) { if (downloadManager == null) { downloadManager = @@ -160,6 +165,7 @@ public final class DemoUtil { } } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static synchronized DatabaseProvider getDatabaseProvider(Context context) { if (databaseProvider == null) { databaseProvider = new StandaloneDatabaseProvider(context); @@ -177,6 +183,7 @@ public final class DemoUtil { return downloadDirectory; } + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static CacheDataSource.Factory buildReadOnlyCacheDataSource( DataSource.Factory upstreamFactory, Cache cache) { return new CacheDataSource.Factory() diff --git a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java index 92349a575e..a303815125 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/DownloadTracker.java @@ -23,6 +23,7 @@ import android.net.Uri; import android.os.AsyncTask; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.annotation.RequiresApi; import androidx.fragment.app.FragmentManager; import androidx.media3.common.DrmInitData; @@ -53,6 +54,7 @@ import java.util.HashMap; import java.util.concurrent.CopyOnWriteArraySet; /** Tracks media that has been downloaded. */ +@OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) public class DownloadTracker { /** Listens for changes in the tracked downloads. */ diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index 6b765679ad..3d2586a615 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -41,6 +41,7 @@ import android.widget.ImageButton; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; @@ -120,6 +121,7 @@ public class SampleChooserActivity extends AppCompatActivity } /** Start the download service if it should be running but it's not currently. */ + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private void startDownloadService() { // Starting the service in the foreground causes notification flicker if there is no scheduled // action. Starting it in the background throws an exception if the app is in the background too @@ -274,6 +276,7 @@ public class SampleChooserActivity extends AppCompatActivity private boolean sawError; + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) @Override protected List doInBackground(String... uris) { List result = new ArrayList<>(); From 6858fe91169e87b540e6f44558fa439c48914de5 Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Fri, 25 Mar 2022 19:48:33 +0000 Subject: [PATCH 007/116] Transformer Demo: Add 8k24fps video option. This can help allow Transformer be more aware of issues in high-res videos. PiperOrigin-RevId: 437313282 --- .../androidx/media3/demo/transformer/ConfigurationActivity.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index eaa51847e5..a24ab9e46e 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -61,6 +61,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "https://storage.googleapis.com/exoplayer-test-media-0/BigBuckBunny_320x180.mp4", "https://html5demos.com/assets/dizzy.webm", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", }; private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS "MP4 with H264 video and AAC audio", @@ -68,6 +69,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "Long MP4 with H264 video and AAC audio", "WebM with VP8 video and Vorbis audio", "4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)", + "8k 24fps MP4 with H265 video and AAC audio", }; private static final String SAME_AS_INPUT_OPTION = "same as input"; From 1b52739dfbf49850a5db1b02739d9a2521447e0e Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Mar 2022 12:41:12 +0100 Subject: [PATCH 008/116] Switch the demo app from Util.areEqual to Guava's Objects.equals Util.areEqual will not be part of the stable API. PiperOrigin-RevId: 437723080 --- .../java/androidx/media3/demo/main/SampleChooserActivity.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java index 3d2586a615..fa61a3ba44 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/SampleChooserActivity.java @@ -54,6 +54,7 @@ import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; import androidx.media3.exoplayer.RenderersFactory; import androidx.media3.exoplayer.offline.DownloadService; +import com.google.common.base.Objects; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.IOException; @@ -487,7 +488,7 @@ public class SampleChooserActivity extends AppCompatActivity private PlaylistGroup getGroup(String groupName, List groups) { for (int i = 0; i < groups.size(); i++) { - if (Util.areEqual(groupName, groups.get(i).title)) { + if (Objects.equal(groupName, groups.get(i).title)) { return groups.get(i); } } From 827cf51dc9bb5a9aa33e2e4a15788c6cf10e32db Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 28 Mar 2022 13:30:05 +0100 Subject: [PATCH 009/116] Check thread name for GL methods. The thread name is used to verify the thread in both createOpenGlObjectsAndInitializeFrameProcessors() and processFrame(). Also remove glThread field that was only used for this verification. PiperOrigin-RevId: 437730804 --- .../androidx/media3/transformer/FrameProcessorChain.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 687dbeff19..76463dc732 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -72,8 +72,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final EGLContext eglContext; /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ private final ExecutorService singleThreadExecutorService; - /** The {@link #singleThreadExecutorService} thread. */ - private @MonotonicNonNull Thread glThread; /** Futures corresponding to the executor service's pending tasks. */ private final ConcurrentLinkedQueue> futures; /** Number of frames {@link #registerInputFrame() registered} but not fully processed. */ @@ -355,7 +353,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @EnsuresNonNull("eglSurface") private Void createOpenGlObjectsAndInitializeFrameProcessors( Surface outputSurface, @Nullable SurfaceView debugSurfaceView) throws IOException { - glThread = Thread.currentThread(); + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + if (enableExperimentalHdrEditing) { // TODO(b/209404935): Don't assume BT.2020 PQ input/output. eglSurface = GlUtil.getEglSurfaceBt2020Pq(eglDisplay, outputSurface); @@ -396,7 +395,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ @RequiresNonNull({"inputSurfaceTexture", "eglSurface"}) private void processFrame() { - checkState(Thread.currentThread().equals(glThread)); + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); if (frameProcessors.isEmpty()) { GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); From 79db98e73354f013045421b1078a3f4ac3b99780 Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Mar 2022 15:31:19 +0100 Subject: [PATCH 010/116] Remove unnecessary initialization PiperOrigin-RevId: 437753013 --- .../java/androidx/media3/transformer/TransformationRequest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index 94c3454ddd..7658dc61d8 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -51,7 +51,6 @@ public final class TransformationRequest { public Builder() { scaleX = 1; scaleY = 1; - rotationDegrees = 0; outputHeight = C.LENGTH_UNSET; } From d2a9419ad377fcbb35ddfd49dc94be58b041fbea Mon Sep 17 00:00:00 2001 From: andrewlewis Date: Mon, 28 Mar 2022 16:29:56 +0100 Subject: [PATCH 011/116] Add support for requesting color transfer to SDR From Android T onwards `MediaCodec` supports requesting tone-mapping down to SDR. Add an option to request this behavior and document that it isn't supported before T. Also add an option in the demo app to try it out. Tested manually on a prerelease build. PiperOrigin-RevId: 437765325 --- .../transformer/ConfigurationActivity.java | 11 +++++++ .../demo/transformer/TransformerActivity.java | 2 ++ .../res/layout/configuration_activity.xml | 10 +++++++ .../src/main/res/values/strings.xml | 3 +- .../androidx/media3/transformer/Codec.java | 4 ++- .../transformer/DefaultDecoderFactory.java | 7 ++++- .../transformer/TransformationRequest.java | 30 ++++++++++++++++++- .../transformer/TransformerVideoRenderer.java | 3 ++ .../VideoTranscodingSamplePipeline.java | 5 +++- 9 files changed, 70 insertions(+), 5 deletions(-) diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index a24ab9e46e..bc0219f6f5 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -54,6 +54,7 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String SCALE_Y = "scale_y"; public static final String ROTATE_DEGREES = "rotate_degrees"; public static final String ENABLE_FALLBACK = "enable_fallback"; + public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; private static final String[] INPUT_URIS = { "https://html5demos.com/assets/dizzy.mp4", @@ -84,6 +85,7 @@ public final class ConfigurationActivity extends AppCompatActivity { private @MonotonicNonNull Spinner scaleSpinner; private @MonotonicNonNull Spinner rotateSpinner; private @MonotonicNonNull CheckBox enableFallbackCheckBox; + private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; private int inputUriPosition; @@ -150,6 +152,7 @@ public final class ConfigurationActivity extends AppCompatActivity { rotateAdapter.addAll(SAME_AS_INPUT_OPTION, "0", "10", "45", "60", "90", "180"); enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); + enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); } @@ -179,6 +182,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "scaleSpinner", "rotateSpinner", "enableFallbackCheckBox", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void startTransformation(View view) { @@ -211,6 +215,8 @@ public final class ConfigurationActivity extends AppCompatActivity { bundle.putFloat(ROTATE_DEGREES, Float.parseFloat(selectedRotate)); } bundle.putBoolean(ENABLE_FALLBACK, enableFallbackCheckBox.isChecked()); + bundle.putBoolean( + ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); transformerIntent.putExtras(bundle); @@ -243,6 +249,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void onRemoveAudio(View view) { @@ -261,6 +268,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void onRemoveVideo(View view) { @@ -278,6 +286,7 @@ public final class ConfigurationActivity extends AppCompatActivity { "resolutionHeightSpinner", "scaleSpinner", "rotateSpinner", + "enableRequestSdrToneMappingCheckBox", "enableHdrEditingCheckBox" }) private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { @@ -286,6 +295,7 @@ public final class ConfigurationActivity extends AppCompatActivity { resolutionHeightSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); + enableRequestSdrToneMappingCheckBox.setEnabled(isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); @@ -293,6 +303,7 @@ public final class ConfigurationActivity extends AppCompatActivity { findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled); + findViewById(R.id.request_sdr_tone_mapping).setEnabled(isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); } } diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index ad83ce75f0..3a957cba3e 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -225,6 +225,8 @@ public final class TransformerActivity extends AppCompatActivity { bundle.getFloat(ConfigurationActivity.ROTATE_DEGREES, /* defaultValue= */ 0); requestBuilder.setRotationDegrees(rotateDegrees); + requestBuilder.setEnableRequestSdrToneMapping( + bundle.getBoolean(ConfigurationActivity.ENABLE_REQUEST_SDR_TONE_MAPPING)); requestBuilder.experimental_setEnableHdrEditing( bundle.getBoolean(ConfigurationActivity.ENABLE_HDR_EDITING)); transformerBuilder diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index 1ff3cafc6b..c973bf4137 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -169,6 +169,16 @@ android:layout_gravity="right" android:checked="true"/> + + + + diff --git a/demos/transformer/src/main/res/values/strings.xml b/demos/transformer/src/main/res/values/strings.xml index 8e8f97ecf9..8ab9fe25f2 100644 --- a/demos/transformer/src/main/res/values/strings.xml +++ b/demos/transformer/src/main/res/values/strings.xml @@ -27,8 +27,9 @@ Scale video Rotate video (degrees) Enable fallback - Transform + Request SDR tone-mapping [Experimental] HDR editing + Transform Debug preview: No debug preview available. Transformation started diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java index 104a48ccbd..bf04a863cf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java @@ -58,10 +58,12 @@ public interface Codec { * @param format The {@link Format} (of the input data) used to determine the underlying decoder * and its configuration values. * @param outputSurface The {@link Surface} to which the decoder output is rendered. + * @param enableRequestSdrToneMapping Whether to request tone-mapping to SDR. * @return A {@link Codec} for video decoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ - Codec createForVideoDecoding(Format format, Surface outputSurface) + Codec createForVideoDecoding( + Format format, Surface outputSurface, boolean enableRequestSdrToneMapping) throws TransformationException; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java index a044351682..7b525416a3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultDecoderFactory.java @@ -49,7 +49,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } @Override - public Codec createForVideoDecoding(Format format, Surface outputSurface) + public Codec createForVideoDecoding( + Format format, Surface outputSurface, boolean enableRequestSdrToneMapping) throws TransformationException { MediaFormat mediaFormat = MediaFormat.createVideoFormat( @@ -63,6 +64,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; // cycle. This key ensures no frame dropping when the decoder's output surface is full. mediaFormat.setInteger(MediaFormat.KEY_ALLOW_FRAME_DROP, 0); } + if (SDK_INT >= 31 && enableRequestSdrToneMapping) { + mediaFormat.setInteger( + MediaFormat.KEY_COLOR_TRANSFER_REQUEST, MediaFormat.COLOR_TRANSFER_SDR_VIDEO); + } @Nullable String mediaCodecName = EncoderUtil.findCodecForFormat(mediaFormat, /* isDecoder= */ true); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index 7658dc61d8..a0ca504c08 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -40,6 +40,7 @@ public final class TransformationRequest { private int outputHeight; @Nullable private String audioMimeType; @Nullable private String videoMimeType; + private boolean enableRequestSdrToneMapping; private boolean enableHdrEditing; /** @@ -62,6 +63,7 @@ public final class TransformationRequest { this.outputHeight = transformationRequest.outputHeight; this.audioMimeType = transformationRequest.audioMimeType; this.videoMimeType = transformationRequest.videoMimeType; + this.enableRequestSdrToneMapping = transformationRequest.enableRequestSdrToneMapping; this.enableHdrEditing = transformationRequest.enableHdrEditing; } @@ -194,13 +196,30 @@ public final class TransformationRequest { return this; } + /** + * Sets whether to request tone-mapping to standard dynamic range (SDR). If enabled and + * supported, high dynamic range (HDR) input will be tone-mapped into an SDR opto-electrical + * transfer function before processing. + * + *

The setting has no effect if the input is already in SDR, or if tone-mapping is not + * supported. Currently tone-mapping is only guaranteed to be supported from Android T onwards. + * + * @param enableRequestSdrToneMapping Whether to request tone-mapping down to SDR. + * @return This builder. + */ + public Builder setEnableRequestSdrToneMapping(boolean enableRequestSdrToneMapping) { + this.enableRequestSdrToneMapping = enableRequestSdrToneMapping; + return this; + } + /** * Sets whether to attempt to process any input video stream as a high dynamic range (HDR) * signal. * *

This method is experimental, and will be renamed or removed in a future release. The HDR * editing feature is under development and is intended for developing/testing HDR processing - * and encoding support. + * and encoding support. HDR editing can't be enabled at the same time as {@link + * #setEnableRequestSdrToneMapping(boolean) SDR tone-mapping}. * * @param enableHdrEditing Whether to attempt to process any input video stream as a high * dynamic range (HDR) signal. @@ -221,6 +240,7 @@ public final class TransformationRequest { outputHeight, audioMimeType, videoMimeType, + enableRequestSdrToneMapping, enableHdrEditing); } } @@ -271,6 +291,9 @@ public final class TransformationRequest { * @see Builder#setVideoMimeType(String) */ @Nullable public final String videoMimeType; + /** Whether to request tone-mapping to standard dynamic range (SDR). */ + public final boolean enableRequestSdrToneMapping; + /** * Whether to attempt to process any input video stream as a high dynamic range (HDR) signal. * @@ -286,7 +309,9 @@ public final class TransformationRequest { int outputHeight, @Nullable String audioMimeType, @Nullable String videoMimeType, + boolean enableRequestSdrToneMapping, boolean enableHdrEditing) { + checkArgument(!enableHdrEditing || !enableRequestSdrToneMapping); this.flattenForSlowMotion = flattenForSlowMotion; this.scaleX = scaleX; this.scaleY = scaleY; @@ -294,6 +319,7 @@ public final class TransformationRequest { this.outputHeight = outputHeight; this.audioMimeType = audioMimeType; this.videoMimeType = videoMimeType; + this.enableRequestSdrToneMapping = enableRequestSdrToneMapping; this.enableHdrEditing = enableHdrEditing; } @@ -313,6 +339,7 @@ public final class TransformationRequest { && outputHeight == that.outputHeight && Util.areEqual(audioMimeType, that.audioMimeType) && Util.areEqual(videoMimeType, that.videoMimeType) + && enableRequestSdrToneMapping == that.enableRequestSdrToneMapping && enableHdrEditing == that.enableHdrEditing; } @@ -325,6 +352,7 @@ public final class TransformationRequest { result = 31 * result + outputHeight; result = 31 * result + (audioMimeType != null ? audioMimeType.hashCode() : 0); result = 31 * result + (videoMimeType != null ? videoMimeType.hashCode() : 0); + result = 31 * result + (enableRequestSdrToneMapping ? 1 : 0); result = 31 * result + (enableHdrEditing ? 1 : 0); return result; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java index b3042a922b..d8789fc382 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformerVideoRenderer.java @@ -107,6 +107,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (encoderFactory.videoNeedsEncoding()) { return false; } + if (transformationRequest.enableRequestSdrToneMapping) { + return false; + } if (transformationRequest.enableHdrEditing) { return false; } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 5b4da873c7..068495d5e2 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -124,7 +124,10 @@ import org.checkerframework.dataflow.qual.Pure; encoderSupportedFormat.width, encoderSupportedFormat.height)); decoder = - decoderFactory.createForVideoDecoding(inputFormat, frameProcessorChain.getInputSurface()); + decoderFactory.createForVideoDecoding( + inputFormat, + frameProcessorChain.getInputSurface(), + transformationRequest.enableRequestSdrToneMapping); maxPendingFrameCount = getMaxPendingFrameCount(); } From 0724e2b99f7f9e1b1c68b93d87955d109d371731 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 28 Mar 2022 17:17:03 +0100 Subject: [PATCH 012/116] Check if there is a current context before generating textures/FBOs. This avoids silent failures where the generated identifiers are 0. PiperOrigin-RevId: 437775689 --- .../src/main/java/androidx/media3/common/util/GlUtil.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index 8fca7eb310..f29b8f0503 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -371,6 +371,9 @@ public final class GlUtil { * GLES11Ext#GL_TEXTURE_EXTERNAL_OES} for an external texture. */ private static int generateAndBindTexture(int textureTarget) { + checkEglException( + !Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context"); + int[] texId = new int[1]; GLES20.glGenTextures(/* n= */ 1, texId, /* offset= */ 0); checkGlError(); @@ -391,6 +394,9 @@ public final class GlUtil { * @param texId The identifier of the texture to attach to the framebuffer. */ public static int createFboForTexture(int texId) { + checkEglException( + !Util.areEqual(EGL14.eglGetCurrentContext(), EGL14.EGL_NO_CONTEXT), "No current context"); + int[] fboId = new int[1]; GLES20.glGenFramebuffers(/* n= */ 1, fboId, /* offset= */ 0); checkGlError(); From 2a2873840d48f137c58db19dce7e9044ee3e9811 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Mar 2022 17:24:56 +0100 Subject: [PATCH 013/116] Add PlayerView to the stable API PiperOrigin-RevId: 437777445 --- .../java/androidx/media3/ui/PlayerView.java | 53 +++++++++++++++++-- 1 file changed, 48 insertions(+), 5 deletions(-) diff --git a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java index b88da6bafa..ef7e22325c 100644 --- a/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java +++ b/libraries/ui/src/main/java/androidx/media3/ui/PlayerView.java @@ -43,6 +43,7 @@ import android.view.ViewGroup; import android.widget.FrameLayout; import android.widget.ImageView; import android.widget.TextView; +import androidx.annotation.ColorInt; import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; @@ -168,30 +169,30 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * by drawables with the same names defined in your application. See the {@link PlayerControlView} * documentation for a list of drawables that can be overridden. */ -@UnstableApi public class PlayerView extends FrameLayout implements AdViewProvider { /** * Determines when the buffering view is shown. One of {@link #SHOW_BUFFERING_NEVER}, {@link * #SHOW_BUFFERING_WHEN_PLAYING} or {@link #SHOW_BUFFERING_ALWAYS}. */ + @UnstableApi @Documented @Retention(RetentionPolicy.SOURCE) @Target(TYPE_USE) @IntDef({SHOW_BUFFERING_NEVER, SHOW_BUFFERING_WHEN_PLAYING, SHOW_BUFFERING_ALWAYS}) public @interface ShowBuffering {} /** The buffering view is never shown. */ - public static final int SHOW_BUFFERING_NEVER = 0; + @UnstableApi public static final int SHOW_BUFFERING_NEVER = 0; /** * The buffering view is shown when the player is in the {@link Player#STATE_BUFFERING buffering} * state and {@link Player#getPlayWhenReady() playWhenReady} is {@code true}. */ - public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; + @UnstableApi public static final int SHOW_BUFFERING_WHEN_PLAYING = 1; /** * The buffering view is always shown when the player is in the {@link Player#STATE_BUFFERING * buffering} state. */ - public static final int SHOW_BUFFERING_ALWAYS = 2; + @UnstableApi public static final int SHOW_BUFFERING_ALWAYS = 2; private static final int SURFACE_TYPE_NONE = 0; private static final int SURFACE_TYPE_SURFACE_VIEW = 1; @@ -444,6 +445,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param oldPlayerView The old view to detach from the player. * @param newPlayerView The new view to attach to the player. */ + @UnstableApi public static void switchTargetView( Player player, @Nullable PlayerView oldPlayerView, @Nullable PlayerView newPlayerView) { if (oldPlayerView == newPlayerView) { @@ -539,18 +541,21 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param resizeMode The {@link ResizeMode}. */ + @UnstableApi public void setResizeMode(@ResizeMode int resizeMode) { Assertions.checkStateNotNull(contentFrame); contentFrame.setResizeMode(resizeMode); } /** Returns the {@link ResizeMode}. */ + @UnstableApi public @ResizeMode int getResizeMode() { Assertions.checkStateNotNull(contentFrame); return contentFrame.getResizeMode(); } /** Returns whether artwork is displayed if present in the media. */ + @UnstableApi public boolean getUseArtwork() { return useArtwork; } @@ -560,6 +565,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param useArtwork Whether artwork is displayed. */ + @UnstableApi public void setUseArtwork(boolean useArtwork) { Assertions.checkState(!useArtwork || artworkView != null); if (this.useArtwork != useArtwork) { @@ -569,6 +575,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns the default artwork to display. */ + @UnstableApi @Nullable public Drawable getDefaultArtwork() { return defaultArtwork; @@ -580,6 +587,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param defaultArtwork the default artwork to display */ + @UnstableApi public void setDefaultArtwork(@Nullable Drawable defaultArtwork) { if (this.defaultArtwork != defaultArtwork) { this.defaultArtwork = defaultArtwork; @@ -588,6 +596,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns whether the playback controls can be shown. */ + @UnstableApi public boolean getUseController() { return useController; } @@ -601,6 +610,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param useController Whether the playback controls can be shown. */ + @UnstableApi public void setUseController(boolean useController) { Assertions.checkState(!useController || controller != null); setClickable(useController || hasOnClickListeners()); @@ -622,7 +632,8 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param color The background color. */ - public void setShutterBackgroundColor(int color) { + @UnstableApi + public void setShutterBackgroundColor(@ColorInt int color) { if (shutterView != null) { shutterView.setBackgroundColor(color); } @@ -647,6 +658,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param keepContentOnPlayerReset Whether the currently displayed video frame or media artwork is * kept visible when the player is reset. */ + @UnstableApi public void setKeepContentOnPlayerReset(boolean keepContentOnPlayerReset) { if (this.keepContentOnPlayerReset != keepContentOnPlayerReset) { this.keepContentOnPlayerReset = keepContentOnPlayerReset; @@ -662,6 +674,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * {@link #SHOW_BUFFERING_NEVER}, {@link #SHOW_BUFFERING_WHEN_PLAYING} and {@link * #SHOW_BUFFERING_ALWAYS}. */ + @UnstableApi public void setShowBuffering(@ShowBuffering int showBuffering) { if (this.showBuffering != showBuffering) { this.showBuffering = showBuffering; @@ -674,6 +687,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param errorMessageProvider The error message provider. */ + @UnstableApi public void setErrorMessageProvider( @Nullable ErrorMessageProvider errorMessageProvider) { if (this.errorMessageProvider != errorMessageProvider) { @@ -688,6 +702,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param message The message to display, or {@code null} to clear a previously set message. */ + @UnstableApi public void setCustomErrorMessage(@Nullable CharSequence message) { Assertions.checkState(errorMessageView != null); customErrorMessage = message; @@ -725,11 +740,13 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param event A key event. * @return Whether the key event was handled. */ + @UnstableApi public boolean dispatchMediaKeyEvent(KeyEvent event) { return useController() && controller.dispatchMediaKeyEvent(event); } /** Returns whether the controller is currently fully visible. */ + @UnstableApi public boolean isControllerFullyVisible() { return controller != null && controller.isFullyVisible(); } @@ -741,11 +758,13 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * #getControllerShowTimeoutMs()}}. They are shown indefinitely when playback has not started yet, * is paused, has ended or failed. */ + @UnstableApi public void showController() { showController(shouldShowControllerIndefinitely()); } /** Hides the playback controls. Does nothing if playback controls are disabled. */ + @UnstableApi public void hideController() { if (controller != null) { controller.hide(); @@ -760,6 +779,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The timeout in milliseconds. A non-positive value will cause the controller to remain * visible indefinitely. */ + @UnstableApi public int getControllerShowTimeoutMs() { return controllerShowTimeoutMs; } @@ -771,6 +791,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param controllerShowTimeoutMs The timeout in milliseconds. A non-positive value will cause the * controller to remain visible indefinitely. */ + @UnstableApi public void setControllerShowTimeoutMs(int controllerShowTimeoutMs) { Assertions.checkStateNotNull(controller); this.controllerShowTimeoutMs = controllerShowTimeoutMs; @@ -781,6 +802,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { } /** Returns whether the playback controls are hidden by touch events. */ + @UnstableApi public boolean getControllerHideOnTouch() { return controllerHideOnTouch; } @@ -790,6 +812,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerHideOnTouch Whether the playback controls are hidden by touch events. */ + @UnstableApi public void setControllerHideOnTouch(boolean controllerHideOnTouch) { Assertions.checkStateNotNull(controller); this.controllerHideOnTouch = controllerHideOnTouch; @@ -801,6 +824,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * ends, or fails. If set to false, the playback controls can be manually operated with {@link * #showController()} and {@link #hideController()}. */ + @UnstableApi public boolean getControllerAutoShow() { return controllerAutoShow; } @@ -812,6 +836,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerAutoShow Whether the playback controls are allowed to show automatically. */ + @UnstableApi public void setControllerAutoShow(boolean controllerAutoShow) { this.controllerAutoShow = controllerAutoShow; } @@ -822,6 +847,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param controllerHideDuringAds Whether the playback controls are hidden when ads are playing. */ + @UnstableApi public void setControllerHideDuringAds(boolean controllerHideDuringAds) { this.controllerHideDuringAds = controllerHideDuringAds; } @@ -832,6 +858,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified about visibility changes, or null to remove the * current listener. */ + @UnstableApi public void setControllerVisibilityListener( @Nullable PlayerControlView.VisibilityListener listener) { Assertions.checkStateNotNull(controller); @@ -853,6 +880,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified when the fullscreen button is clicked, or null to * remove the current listener and hide the fullscreen button. */ + @UnstableApi public void setControllerOnFullScreenModeChangedListener( @Nullable PlayerControlView.OnFullScreenModeChangedListener listener) { Assertions.checkStateNotNull(controller); @@ -864,6 +892,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showRewindButton Whether the rewind button is shown. */ + @UnstableApi public void setShowRewindButton(boolean showRewindButton) { Assertions.checkStateNotNull(controller); controller.setShowRewindButton(showRewindButton); @@ -874,6 +903,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showFastForwardButton Whether the fast forward button is shown. */ + @UnstableApi public void setShowFastForwardButton(boolean showFastForwardButton) { Assertions.checkStateNotNull(controller); controller.setShowFastForwardButton(showFastForwardButton); @@ -884,6 +914,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showPreviousButton Whether the previous button is shown. */ + @UnstableApi public void setShowPreviousButton(boolean showPreviousButton) { Assertions.checkStateNotNull(controller); controller.setShowPreviousButton(showPreviousButton); @@ -894,6 +925,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showNextButton Whether the next button is shown. */ + @UnstableApi public void setShowNextButton(boolean showNextButton) { Assertions.checkStateNotNull(controller); controller.setShowNextButton(showNextButton); @@ -904,6 +936,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param repeatToggleModes A set of {@link RepeatModeUtil.RepeatToggleModes}. */ + @UnstableApi public void setRepeatToggleModes(@RepeatModeUtil.RepeatToggleModes int repeatToggleModes) { Assertions.checkStateNotNull(controller); controller.setRepeatToggleModes(repeatToggleModes); @@ -914,6 +947,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showShuffleButton Whether the shuffle button is shown. */ + @UnstableApi public void setShowShuffleButton(boolean showShuffleButton) { Assertions.checkStateNotNull(controller); controller.setShowShuffleButton(showShuffleButton); @@ -924,6 +958,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showSubtitleButton Whether the subtitle button is shown. */ + @UnstableApi public void setShowSubtitleButton(boolean showSubtitleButton) { Assertions.checkStateNotNull(controller); controller.setShowSubtitleButton(showSubtitleButton); @@ -934,6 +969,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showVrButton Whether the vr button is shown. */ + @UnstableApi public void setShowVrButton(boolean showVrButton) { Assertions.checkStateNotNull(controller); controller.setShowVrButton(showVrButton); @@ -944,6 +980,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * * @param showMultiWindowTimeBar Whether to show all windows. */ + @UnstableApi public void setShowMultiWindowTimeBar(boolean showMultiWindowTimeBar) { Assertions.checkStateNotNull(controller); controller.setShowMultiWindowTimeBar(showMultiWindowTimeBar); @@ -959,6 +996,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param extraPlayedAdGroups Whether each ad has been played, or {@code null} to show no extra ad * markers. */ + @UnstableApi public void setExtraAdGroupMarkers( @Nullable long[] extraAdGroupTimesMs, @Nullable boolean[] extraPlayedAdGroups) { Assertions.checkStateNotNull(controller); @@ -971,6 +1009,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param listener The listener to be notified about aspect ratios changes of the video content or * the content frame. */ + @UnstableApi public void setAspectRatioListener( @Nullable AspectRatioFrameLayout.AspectRatioListener listener) { Assertions.checkStateNotNull(contentFrame); @@ -994,6 +1033,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The {@link SurfaceView}, {@link TextureView}, {@code SphericalGLSurfaceView}, {@code * VideoDecoderGLSurfaceView} or {@code null}. */ + @UnstableApi @Nullable public View getVideoSurfaceView() { return surfaceView; @@ -1006,6 +1046,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The overlay {@link FrameLayout}, or {@code null} if the layout has been customized and * the overlay is not present. */ + @UnstableApi @Nullable public FrameLayout getOverlayFrameLayout() { return overlayFrameLayout; @@ -1017,6 +1058,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @return The {@link SubtitleView}, or {@code null} if the layout has been customized and the * subtitle view is not present. */ + @UnstableApi @Nullable public SubtitleView getSubtitleView() { return subtitleView; @@ -1070,6 +1112,7 @@ public class PlayerView extends FrameLayout implements AdViewProvider { * @param contentFrame The content frame, or {@code null}. * @param aspectRatio The aspect ratio to apply. */ + @UnstableApi protected void onContentAspectRatioChanged( @Nullable AspectRatioFrameLayout contentFrame, float aspectRatio) { if (contentFrame != null) { From e4556d76a992af5085ae673b8902be8459334f60 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Mar 2022 17:26:03 +0100 Subject: [PATCH 014/116] Replace Util.SDK_INT with Build.VERSION.SDK_INT in the demo app Util.SDK_INT will not be part of the stable API. This change only touches those parts of the main demo app that will not be opted-in to the unstable API for other reasons (e.g. download use-cases). PiperOrigin-RevId: 437777687 --- .../androidx/media3/demo/main/PlayerActivity.java | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index d158059a9f..ed33dea550 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -17,6 +17,7 @@ package androidx.media3.demo.main; import android.content.Intent; import android.content.pm.PackageManager; +import android.os.Build; import android.os.Bundle; import android.util.Pair; import android.view.KeyEvent; @@ -142,7 +143,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStart() { super.onStart(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -153,7 +154,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onResume() { super.onResume(); - if (Util.SDK_INT <= 23 || player == null) { + if (Build.VERSION.SDK_INT <= 23 || player == null) { initializePlayer(); if (playerView != null) { playerView.onResume(); @@ -164,7 +165,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onPause() { super.onPause(); - if (Util.SDK_INT <= 23) { + if (Build.VERSION.SDK_INT <= 23) { if (playerView != null) { playerView.onPause(); } @@ -175,7 +176,7 @@ public class PlayerActivity extends AppCompatActivity @Override public void onStop() { super.onStop(); - if (Util.SDK_INT > 23) { + if (Build.VERSION.SDK_INT > 23) { if (playerView != null) { playerView.onPause(); } @@ -342,7 +343,7 @@ public class PlayerActivity extends AppCompatActivity MediaItem.DrmConfiguration drmConfiguration = mediaItem.localConfiguration.drmConfiguration; if (drmConfiguration != null) { - if (Util.SDK_INT < 18) { + if (Build.VERSION.SDK_INT < 18) { showToast(R.string.error_drm_unsupported_before_api_18); finish(); return Collections.emptyList(); From 0096b40b7521906236d11649ab4549a9c902c386 Mon Sep 17 00:00:00 2001 From: ibaker Date: Mon, 28 Mar 2022 17:26:47 +0100 Subject: [PATCH 015/116] More demo app unstable API opt-in and reshuffling Follow-up to https://github.com/androidx/media/commit/a5330d43d4c3b5c464d9d35d39c0185216587c90 PiperOrigin-RevId: 437777871 --- .../media3/demo/main/PlayerActivity.java | 45 ++++++++++--------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index ed33dea550..f556d8c8cb 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -28,6 +28,7 @@ import android.widget.LinearLayout; import android.widget.TextView; import android.widget.Toast; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.appcompat.app.AppCompatActivity; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; @@ -509,29 +510,29 @@ public class PlayerActivity extends AppCompatActivity private static List createMediaItems(Intent intent, DownloadTracker downloadTracker) { List mediaItems = new ArrayList<>(); for (MediaItem item : IntentUtil.createMediaItemsFromIntent(intent)) { - @Nullable - DownloadRequest downloadRequest = - downloadTracker.getDownloadRequest(item.localConfiguration.uri); - if (downloadRequest != null) { - MediaItem.Builder builder = item.buildUpon(); - builder - .setMediaId(downloadRequest.id) - .setUri(downloadRequest.uri) - .setCustomCacheKey(downloadRequest.customCacheKey) - .setMimeType(downloadRequest.mimeType) - .setStreamKeys(downloadRequest.streamKeys); - @Nullable - MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; - if (drmConfiguration != null) { - builder.setDrmConfiguration( - drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); - } - - mediaItems.add(builder.build()); - } else { - mediaItems.add(item); - } + mediaItems.add( + maybeSetDownloadProperties( + item, downloadTracker.getDownloadRequest(item.localConfiguration.uri))); } return mediaItems; } + + @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) + private static MediaItem maybeSetDownloadProperties( + MediaItem item, @Nullable DownloadRequest downloadRequest) { + MediaItem.Builder builder = item.buildUpon(); + builder + .setMediaId(downloadRequest.id) + .setUri(downloadRequest.uri) + .setCustomCacheKey(downloadRequest.customCacheKey) + .setMimeType(downloadRequest.mimeType) + .setStreamKeys(downloadRequest.streamKeys); + @Nullable + MediaItem.DrmConfiguration drmConfiguration = item.localConfiguration.drmConfiguration; + if (drmConfiguration != null) { + builder.setDrmConfiguration( + drmConfiguration.buildUpon().setKeySetId(downloadRequest.keySetId).build()); + } + return builder.build(); + } } From f722114fc095a0fb6d2261191f481a29c9ed3531 Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 29 Mar 2022 09:25:54 +0100 Subject: [PATCH 016/116] Add a Kotlin example for opting-in to the unstable API PiperOrigin-RevId: 437962027 --- .../media3/common/util/UnstableApi.java | 23 +++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java b/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java index a624093e08..e4a7f5c20e 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/UnstableApi.java @@ -47,8 +47,27 @@ import java.lang.annotation.Target; * Android Studio, in order to alert developers to the risk of breaking changes. * *

Individual usage sites can be opted-in to suppress the lint error by using the {@link - * androidx.annotation.OptIn} annotation: {@code @androidx.annotation.OptIn(markerClass = - * androidx.media3.common.util.UnstableApi.class)}. + * androidx.annotation.OptIn} annotation. + * + *

In Java: + * + *

{@code
+ * import androidx.annotation.OptIn;
+ * import androidx.media3.common.util.UnstableApi;
+ * ...
+ * @OptIn(markerClass = UnstableApi.class)
+ * private void methodUsingUnstableApis() { ... }
+ * }
+ * + *

In Kotlin: + * + *

{@code
+ * import androidx.annotation.OptIn
+ * import androidx.media3.common.util.UnstableApi
+ * ...
+ * @OptIn(UnstableApi::class)
+ * private fun methodUsingUnstableApis() { ... }
+ * }
* *

Whole projects can be opted-in by suppressing the specific lint error in their {@code lint.xml} file: From 9d48cff9ffb4f1f9d2495838ae783978cd30b98b Mon Sep 17 00:00:00 2001 From: ibaker Date: Tue, 29 Mar 2022 10:07:23 +0100 Subject: [PATCH 017/116] Remove IntDef warning suppression from DefaultTrackSelector The problem is not the IntDef array, it's the fact the lint tool is unable to correctly infer the annotations on the lambda parameters without them being explicitly annotated. It seems explicitly annotating is better than suppressing all IntDef warnings in the whole method. PiperOrigin-RevId: 437969271 --- .../exoplayer/trackselection/DefaultTrackSelector.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java index 2d6297635d..817380a3d2 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/trackselection/DefaultTrackSelector.java @@ -1736,7 +1736,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectVideoTrack( MappedTrackInfo mappedTrackInfo, @@ -1748,7 +1747,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_VIDEO, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> VideoTrackInfo.createForTrackGroup( rendererIndex, group, params, support, mixedMimeTypeSupports[rendererIndex]), VideoTrackInfo::compareSelections); @@ -1770,7 +1769,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectAudioTrack( MappedTrackInfo mappedTrackInfo, @@ -1791,7 +1789,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_AUDIO, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> AudioTrackInfo.createForTrackGroup( rendererIndex, group, params, support, hasVideoRendererWithMappedTracksFinal), AudioTrackInfo::compareSelections); @@ -1813,7 +1811,6 @@ public class DefaultTrackSelector extends MappingTrackSelector { * renderer index, or null if no selection was made. * @throws ExoPlaybackException If an error occurs while selecting the tracks. */ - @SuppressLint("WrongConstant") // Lint doesn't understand arrays of IntDefs. @Nullable protected Pair selectTextTrack( MappedTrackInfo mappedTrackInfo, @@ -1825,7 +1822,7 @@ public class DefaultTrackSelector extends MappingTrackSelector { C.TRACK_TYPE_TEXT, mappedTrackInfo, rendererFormatSupports, - (rendererIndex, group, support) -> + (int rendererIndex, TrackGroup group, @Capabilities int[] support) -> TextTrackInfo.createForTrackGroup( rendererIndex, group, params, support, selectedAudioLanguage), TextTrackInfo::compareSelections); From 808909289c47f454eb242a47a13688a03cdff457 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 29 Mar 2022 12:44:15 +0100 Subject: [PATCH 018/116] Use @linkplain for link text that doesn't match symbol name. PiperOrigin-RevId: 437992927 --- .../transformer/AdvancedFrameProcessor.java | 9 ++--- .../androidx/media3/transformer/Codec.java | 35 ++++++++++--------- .../transformer/DefaultEncoderFactory.java | 24 ++++++------- .../media3/transformer/EncoderSelector.java | 4 +-- .../media3/transformer/EncoderUtil.java | 33 ++++++++--------- .../ExternalCopyFrameProcessor.java | 2 +- .../media3/transformer/FallbackListener.java | 5 +-- .../transformer/FrameProcessorChain.java | 32 ++++++++--------- .../media3/transformer/FrameworkMuxer.java | 12 +++---- .../media3/transformer/GlFrameProcessor.java | 4 +-- .../androidx/media3/transformer/Muxer.java | 28 ++++++++------- .../media3/transformer/MuxerWrapper.java | 28 +++++++-------- .../transformer/SefSlowMotionFlattener.java | 4 +-- .../transformer/TransformationException.java | 2 +- .../transformer/TransformationRequest.java | 14 ++++---- .../media3/transformer/Transformer.java | 20 +++++------ .../transformer/VideoEncoderSettings.java | 2 +- 17 files changed, 132 insertions(+), 126 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index c450b61e44..dc9c73dd55 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -28,10 +28,11 @@ import java.io.IOException; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Applies a transformation matrix in the vertex shader. Operations are done on normalized device - * coordinates (-1 to 1 on x and y axes). No automatic adjustments (like done in {@link - * ScaleToFitFrameProcessor}) are applied on the transformation. Width and height are not modified. - * The background color will default to black. + * Applies a transformation matrix in the vertex shader. + * + *

Operations are done on normalized device coordinates (-1 to 1 on x and y axes). No automatic + * adjustments (like done in {@link ScaleToFitFrameProcessor}) are applied on the transformation. + * Width and height are not modified. The background color will default to black. */ @UnstableApi public final class AdvancedFrameProcessor implements GlFrameProcessor { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java index bf04a863cf..9e9f3b3baa 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Codec.java @@ -36,7 +36,7 @@ import java.util.List; @UnstableApi public interface Codec { - /** A factory for {@link Codec decoder} instances. */ + /** A factory for {@linkplain Codec decoder} instances. */ interface DecoderFactory { /** A default {@code DecoderFactory} implementation. */ @@ -67,7 +67,7 @@ public interface Codec { throws TransformationException; } - /** A factory for {@link Codec encoder} instances. */ + /** A factory for {@linkplain Codec encoder} instances. */ interface EncoderFactory { /** A default {@code EncoderFactory} implementation. */ @@ -77,13 +77,13 @@ public interface Codec { * Returns a {@link Codec} for audio encoding. * *

This method must validate that the {@link Codec} is configured to produce one of the - * {@code allowedMimeTypes}. The {@link Format#sampleMimeType sample MIME type} given in {@code - * format} is not necessarily allowed. + * {@code allowedMimeTypes}. The {@linkplain Format#sampleMimeType sample MIME type} given in + * {@code format} is not necessarily allowed. * * @param format The {@link Format} (of the output data) used to determine the underlying * encoder and its configuration values. - * @param allowedMimeTypes The non-empty list of allowed output sample {@link MimeTypes MIME - * types}. + * @param allowedMimeTypes The non-empty list of allowed output sample {@linkplain MimeTypes + * MIME types}. * @return A {@link Codec} for audio encoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ @@ -94,8 +94,8 @@ public interface Codec { * Returns a {@link Codec} for video encoding. * *

This method must validate that the {@link Codec} is configured to produce one of the - * {@code allowedMimeTypes}. The {@link Format#sampleMimeType sample MIME type} given in {@code - * format} is not necessarily allowed. + * {@code allowedMimeTypes}. The {@linkplain Format#sampleMimeType sample MIME type} given in + * {@code format} is not necessarily allowed. * * @param format The {@link Format} (of the output data) used to determine the underlying * encoder and its configuration values. {@link Format#sampleMimeType}, {@link Format#width} @@ -103,8 +103,8 @@ public interface Codec { * Format#rotationDegrees} is 0 and {@link Format#width} {@code >=} {@link Format#height}, * therefore the video is always in landscape orientation. {@link Format#frameRate} is set * to the output video's frame rate, if available. - * @param allowedMimeTypes The non-empty list of allowed output sample {@link MimeTypes MIME - * types}. + * @param allowedMimeTypes The non-empty list of allowed output sample {@linkplain MimeTypes + * MIME types}. * @return A {@link Codec} for video encoding. * @throws TransformationException If no suitable {@link Codec} can be created. */ @@ -142,8 +142,8 @@ public interface Codec { /** * Dequeues a writable input buffer, if available. * - *

This method must not be called from video encoders because they must use {@link Surface - * surfaces} as inputs. + *

This method must not be called from video encoders because they must use a {@link Surface} + * to receive input. * * @param inputBuffer The buffer where the dequeued buffer data is stored, at {@link * DecoderInputBuffer#data inputBuffer.data}. @@ -153,13 +153,13 @@ public interface Codec { boolean maybeDequeueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException; /** - * Queues an input buffer to the {@code Codec}. No buffers may be queued after {@link + * Queues an input buffer to the {@code Codec}. No buffers may be queued after {@linkplain * DecoderInputBuffer#isEndOfStream() end of stream} buffer has been queued. * - *

This method must not be called from video encoders because they must use {@link Surface - * surfaces} as inputs. + *

This method must not be called from video encoders because they must use a {@link Surface} + * to receive input. * - * @param inputBuffer The {@link DecoderInputBuffer input buffer}. + * @param inputBuffer The {@linkplain DecoderInputBuffer input buffer}. * @throws TransformationException If the underlying decoder or encoder encounters a problem. */ void queueInputBuffer(DecoderInputBuffer inputBuffer) throws TransformationException; @@ -169,7 +169,8 @@ public interface Codec { * *

This method must only be called on video encoders because they must use a {@link Surface} as * input. For audio/video decoders or audio encoders, the {@link C#BUFFER_FLAG_END_OF_STREAM} flag - * should be set on the last input buffer {@link #queueInputBuffer(DecoderInputBuffer) queued}. + * should be set on the last input buffer {@linkplain #queueInputBuffer(DecoderInputBuffer) + * queued}. * * @throws TransformationException If the underlying video encoder encounters a problem. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index b59f6ca2bb..70398ced73 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -199,9 +199,9 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } /** - * Finds an {@link MediaCodecInfo encoder} that supports the requested format most closely. + * Finds an {@linkplain MediaCodecInfo encoder} that supports the requested format most closely. * - *

Returns the {@link MediaCodecInfo encoder} and the supported {@link Format} in a {@link + *

Returns the {@linkplain MediaCodecInfo encoder} and the supported {@link Format} in a {@link * Pair}, or {@code null} if none is found. */ @RequiresNonNull("#1.sampleMimeType") @@ -402,22 +402,22 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { private interface EncoderFallbackCost { /** * Returns a cost that represents the gap between the requested encoding parameter(s) and the - * {@link MediaCodecInfo encoder}'s support for them. + * {@linkplain MediaCodecInfo encoder}'s support for them. * - *

The method must return {@link Integer#MAX_VALUE} when the {@link MediaCodecInfo encoder} - * does not support the encoding parameters. + *

The method must return {@link Integer#MAX_VALUE} when the {@linkplain MediaCodecInfo + * encoder} does not support the encoding parameters. */ int getParameterSupportGap(MediaCodecInfo encoderInfo); } /** - * Filters a list of {@link MediaCodecInfo encoders} by a {@link EncoderFallbackCost cost - * function}. + * Filters a list of {@linkplain MediaCodecInfo encoders} by a {@linkplain EncoderFallbackCost + * cost function}. * - * @param encoders A list of {@link MediaCodecInfo encoders}. - * @param cost A {@link EncoderFallbackCost cost function}. - * @return A list of {@link MediaCodecInfo encoders} with the lowest costs, empty if the costs of - * all encoders are {@link Integer#MAX_VALUE}. + * @param encoders A list of {@linkplain MediaCodecInfo encoders}. + * @param cost A {@linkplain EncoderFallbackCost cost function}. + * @return A list of {@linkplain MediaCodecInfo encoders} with the lowest costs, empty if the + * costs of all encoders are {@link Integer#MAX_VALUE}. */ private static ImmutableList filterEncoders( List encoders, EncoderFallbackCost cost, String filterName) { @@ -454,7 +454,7 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { } /** - * Finds a {@link MimeTypes MIME type} that is supported by the encoder and in the {@code + * Finds a {@linkplain MimeTypes MIME type} that is supported by the encoder and in the {@code * allowedMimeTypes}. */ @Nullable diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java index 5d15ac2ed2..c68719a1a4 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderSelector.java @@ -36,8 +36,8 @@ public interface EncoderSelector { * Returns a list of encoders that can encode media in the specified {@code mimeType}, in priority * order. * - * @param mimeType The {@link MimeTypes MIME type} for which an encoder is required. - * @return An unmodifiable list of {@link MediaCodecInfo encoders} that supports the {@code + * @param mimeType The {@linkplain MimeTypes MIME type} for which an encoder is required. + * @return An unmodifiable list of {@linkplain MediaCodecInfo encoders} that support the {@code * mimeType}. The list may be empty. */ List selectEncoderInfos(String mimeType); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java index 8dc0bafc36..e4ef7cb71b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/EncoderUtil.java @@ -47,8 +47,8 @@ public final class EncoderUtil { private static final List encoders = new ArrayList<>(); /** - * Returns a list of {@link MediaCodecInfo encoders} that support the given {@code mimeType}, or - * an empty list if there is none. + * Returns a list of {@linkplain MediaCodecInfo encoders} that support the given {@code mimeType}, + * or an empty list if there is none. */ public static ImmutableList getSupportedEncoders(String mimeType) { maybePopulateEncoderInfos(); @@ -67,22 +67,22 @@ public final class EncoderUtil { } /** - * Finds a {@link MediaCodecInfo encoder}'s supported resolution from a given resolution. + * Finds an {@linkplain MediaCodecInfo encoder}'s supported resolution from a given resolution. * - *

The input resolution is returned, if it (after aligning to the encoders requirement) is - * supported by the {@link MediaCodecInfo encoder}. + *

The input resolution is returned, if it (after aligning to the encoder's requirement) is + * supported by the {@linkplain MediaCodecInfo encoder}. * - *

The resolution will be adjusted to be within the {@link MediaCodecInfo encoder}'s range of - * supported resolutions, and will be aligned to the {@link MediaCodecInfo encoder}'s alignment - * requirement. The adjustment process takes into account the original aspect ratio. But the fixed - * resolution may not preserve the original aspect ratio, depending on the encoder's required size - * alignment. + *

The resolution will be adjusted to be within the {@linkplain MediaCodecInfo encoder}'s range + * of supported resolutions, and will be aligned to the {@linkplain MediaCodecInfo encoder}'s + * alignment requirement. The adjustment process takes into account the original aspect ratio. But + * the fixed resolution may not preserve the original aspect ratio, depending on the encoder's + * required size alignment. * * @param encoderInfo The {@link MediaCodecInfo} of the encoder. * @param mimeType The output MIME type. * @param width The original width. * @param height The original height. - * @return A {@link Size supported resolution}, or {@code null} if unable to find a fallback. + * @return A {@linkplain Size supported resolution}, or {@code null} if unable to find a fallback. */ @Nullable public static Size getSupportedResolution( @@ -136,7 +136,7 @@ public final class EncoderUtil { * Finds the highest supported encoding level given a profile. * * @param encoderInfo The {@link MediaCodecInfo encoderInfo}. - * @param mimeType The {@link MimeTypes MIME type}. + * @param mimeType The {@linkplain MimeTypes MIME type}. * @param profile The encoding profile. * @return The highest supported encoding level, as documented in {@link * MediaCodecInfo.CodecProfileLevel}, or {@link #LEVEL_UNSET} if the profile is not supported. @@ -157,8 +157,8 @@ public final class EncoderUtil { } /** - * Finds a {@link MediaCodec codec} that supports the {@link MediaFormat}, or {@code null} if none - * is found. + * Finds a {@link MediaCodec} that supports the {@link MediaFormat}, or {@code null} if none is + * found. */ @Nullable public static String findCodecForFormat(MediaFormat format, boolean isDecoder) { @@ -183,7 +183,8 @@ public final class EncoderUtil { } /** - * Finds the {@link MediaCodecInfo encoder}'s closest supported bitrate from the given bitrate. + * Finds the {@linkplain MediaCodecInfo encoder}'s closest supported bitrate from the given + * bitrate. */ public static int getClosestSupportedBitrate( MediaCodecInfo encoderInfo, String mimeType, int bitrate) { @@ -203,7 +204,7 @@ public final class EncoderUtil { .isBitrateModeSupported(bitrateMode); } - /** Checks if a {@link MediaCodecInfo codec} is hardware-accelerated. */ + /** Checks if a {@linkplain MediaCodecInfo codec} is hardware-accelerated. */ public static boolean isHardwareAccelerated(MediaCodecInfo encoderInfo, String mimeType) { // TODO(b/214964116): Merge into MediaCodecUtil. if (Util.SDK_INT >= 29) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 64100097e4..71bc45ea1d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -92,7 +92,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; * Sets the texture transform matrix for converting an external surface texture's coordinates to * sampling locations. * - * @param textureTransformMatrix The external surface texture's {@link + * @param textureTransformMatrix The external surface texture's {@linkplain * android.graphics.SurfaceTexture#getTransformMatrix(float[]) transform matrix}. */ public void setTextureTransformMatrix(float[] textureTransformMatrix) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java index 05463fa906..4bcf1c30b6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FallbackListener.java @@ -40,7 +40,8 @@ import androidx.media3.common.util.Util; * Creates a new instance. * * @param mediaItem The {@link MediaItem} to transform. - * @param transformerListeners The {@link Transformer.Listener listeners} to forward events to. + * @param transformerListeners The {@linkplain Transformer.Listener listeners} to forward events + * to. * @param originalTransformationRequest The original {@link TransformationRequest}. */ public FallbackListener( @@ -56,7 +57,7 @@ import androidx.media3.common.util.Util; /** * Registers an output track. * - *

All tracks must be registered before a transformation request is {@link + *

All tracks must be registered before a transformation request is {@linkplain * #onTransformationRequestFinalized(TransformationRequest) finalized}. */ public void registerTrack() { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 76463dc732..8346bde888 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -52,12 +52,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * {@code FrameProcessorChain} applies changes to individual video frames. * - *

Input becomes available on its {@link #getInputSurface() input surface} asynchronously and is - * processed on a background thread as it becomes available. All input frames should be {@link - * #registerInputFrame() registered} before they are rendered to the input surface. {@link - * #getPendingFrameCount()} can be used to check whether there are frames that have not been fully - * processed yet. Output is written to its {@link #configure(Surface, int, int, SurfaceView) output - * surface}. + *

Input becomes available on its {@linkplain #getInputSurface() input surface} asynchronously + * and is processed on a background thread as it becomes available. All input frames should be + * {@linkplain #registerInputFrame() registered} before they are rendered to the input surface. + * {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been + * fully processed yet. Output is written to its {@linkplain #configure(Surface, int, int, + * SurfaceView) output surface}. */ /* package */ final class FrameProcessorChain { @@ -74,7 +74,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final ExecutorService singleThreadExecutorService; /** Futures corresponding to the executor service's pending tasks. */ private final ConcurrentLinkedQueue> futures; - /** Number of frames {@link #registerInputFrame() registered} but not fully processed. */ + /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ private final AtomicInteger pendingFrameCount; /** Prevents further frame processing tasks from being scheduled after {@link #release()}. */ private volatile boolean releaseRequested; @@ -186,7 +186,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Configures the {@code FrameProcessorChain} to process frames to the specified output targets. * - *

This method may only be called once and may override the {@link + *

This method may only be called once and may override the {@linkplain * GlFrameProcessor#configureOutputSize(int, int) output size} of the final {@link * GlFrameProcessor}. * @@ -253,8 +253,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Returns the input {@link Surface}. * - *

The {@code FrameProcessorChain} must be {@link #configure(Surface, int, int, SurfaceView) - * configured}. + *

The {@code FrameProcessorChain} must be {@linkplain #configure(Surface, int, int, + * SurfaceView) configured}. */ public Surface getInputSurface() { checkStateNotNull(inputSurface, "The FrameProcessorChain must be configured."); @@ -296,8 +296,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Returns the number of input frames that have been {@link #registerInputFrame() registered} but - * not completely processed yet. + * Returns the number of input frames that have been {@linkplain #registerInputFrame() registered} + * but not completely processed yet. */ public int getPendingFrameCount() { return pendingFrameCount.get(); @@ -316,9 +316,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Releases all resources. * - *

If the frame processor chain is released before it has {@link #isEnded() ended}, it will - * attempt to cancel processing any input frames that have already become available. Input frames - * that become available after release are ignored. + *

If the frame processor chain is released before it has {@linkplain #isEnded() ended}, it + * will attempt to cancel processing any input frames that have already become available. Input + * frames that become available after release are ignored. */ public void release() { releaseRequested = true; @@ -447,7 +447,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Configures the input and output {@link Size sizes} of a list of {@link GlFrameProcessor + * Configures the input and output {@linkplain Size sizes} of a list of {@link GlFrameProcessor * GlFrameProcessors}. * * @param inputWidth The width of frames passed to the first {@link GlFrameProcessor}, in pixels. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java index 1c433356d4..595bb743d3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameworkMuxer.java @@ -221,13 +221,13 @@ import java.nio.ByteBuffer; } /** - * Converts a {@link MimeTypes MIME type} into a {@link MediaMuxer.OutputFormat MediaMuxer output - * format}. + * Converts a {@linkplain MimeTypes MIME type} into a {@linkplain MediaMuxer.OutputFormat + * MediaMuxer output format}. * - * @param mimeType The {@link MimeTypes MIME type} to convert. - * @return The corresponding {@link MediaMuxer.OutputFormat MediaMuxer output format}. - * @throws IllegalArgumentException If the {@link MimeTypes MIME type} is not supported as output - * format. + * @param mimeType The {@linkplain MimeTypes MIME type} to convert. + * @return The corresponding {@linkplain MediaMuxer.OutputFormat MediaMuxer output format}. + * @throws IllegalArgumentException If the {@linkplain MimeTypes MIME type} is not supported as + * output format. */ private static int mimeTypeToMuxerOutputFormat(String mimeType) { if (mimeType.equals(MimeTypes.VIDEO_MP4)) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index 9bf254965a..84477c06e6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -57,8 +57,8 @@ public interface GlFrameProcessor { /** * Updates the shader program's vertex attributes and uniforms, binds them, and draws. * - *

The frame processor must be {@link #initialize(int) initialized}. The caller is responsible - * for focussing the correct render target before calling this method. + *

The frame processor must be {@linkplain #initialize(int) initialized}. The caller is + * responsible for focussing the correct render target before calling this method. * * @param presentationTimeNs The presentation timestamp of the current frame, in nanoseconds. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java index df8138f837..86bb493476 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Muxer.java @@ -27,12 +27,12 @@ import java.nio.ByteBuffer; /** * Abstracts media muxing operations. * - *

Query whether {@link Factory#supportsOutputMimeType(String) container MIME type} and {@link - * Factory#supportsSampleMimeType(String, String) sample MIME types} are supported and {@link - * #addTrack(Format) add all tracks}, then {@link #writeSampleData(int, ByteBuffer, boolean, long) - * write sample data} to mux samples. Once any sample data has been written, it is not possible to - * add tracks. After writing all sample data, {@link #release(boolean) release} the instance to - * finish writing to the output and return any resources to the system. + *

Query whether {@linkplain Factory#supportsOutputMimeType(String) container MIME type} and + * {@linkplain Factory#supportsSampleMimeType(String, String) sample MIME types} are supported and + * {@linkplain #addTrack(Format) add all tracks}, then {@linkplain #writeSampleData(int, ByteBuffer, + * boolean, long) write sample data} to mux samples. Once any sample data has been written, it is + * not possible to add tracks. After writing all sample data, {@linkplain #release(boolean) release} + * the instance to finish writing to the output and return any resources to the system. */ /* package */ interface Muxer { @@ -55,7 +55,7 @@ import java.nio.ByteBuffer; * Returns a new muxer writing to a file. * * @param path The path to the output file. - * @param outputMimeType The container {@link MimeTypes MIME type} of the output file. + * @param outputMimeType The container {@linkplain MimeTypes MIME type} of the output file. * @throws IllegalArgumentException If the path is invalid or the MIME type is not supported. * @throws IOException If an error occurs opening the output file for writing. */ @@ -68,7 +68,7 @@ import java.nio.ByteBuffer; * output. The file referenced by this ParcelFileDescriptor should not be used before the * muxer is released. It is the responsibility of the caller to close the * ParcelFileDescriptor. This can be done after this method returns. - * @param outputMimeType The {@link MimeTypes MIME type} of the output. + * @param outputMimeType The {@linkplain MimeTypes MIME type} of the output. * @throws IllegalArgumentException If the file descriptor is invalid or the MIME type is not * supported. * @throws IOException If an error occurs opening the output file descriptor for writing. @@ -76,18 +76,20 @@ import java.nio.ByteBuffer; Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) throws IOException; - /** Returns whether the {@link MimeTypes MIME type} provided is a supported output format. */ + /** + * Returns whether the {@linkplain MimeTypes MIME type} provided is a supported output format. + */ boolean supportsOutputMimeType(String mimeType); /** - * Returns whether the sample {@link MimeTypes MIME type} is supported with the given container - * {@link MimeTypes MIME type}. + * Returns whether the sample {@linkplain MimeTypes MIME type} is supported with the given + * container {@linkplain MimeTypes MIME type}. */ boolean supportsSampleMimeType(@Nullable String sampleMimeType, String containerMimeType); /** - * Returns the supported sample {@link MimeTypes MIME types} for the given {@link C.TrackType} - * and container {@link MimeTypes MIME type}. + * Returns the supported sample {@linkplain MimeTypes MIME types} for the given {@link + * C.TrackType} and container {@linkplain MimeTypes MIME type}. */ ImmutableList getSupportedSampleMimeTypes( @C.TrackType int trackType, String containerMimeType); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index 364337b910..5babefd873 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -70,10 +70,10 @@ import java.nio.ByteBuffer; /** * Registers an output track. * - *

All tracks must be registered before any track format is {@link #addTrackFormat(Format) + *

All tracks must be registered before any track format is {@linkplain #addTrackFormat(Format) * added}. * - * @throws IllegalStateException If a track format was {@link #addTrackFormat(Format) added} + * @throws IllegalStateException If a track format was {@linkplain #addTrackFormat(Format) added} * before calling this method. */ public void registerTrack() { @@ -82,14 +82,14 @@ import java.nio.ByteBuffer; trackCount++; } - /** Returns whether the sample {@link MimeTypes MIME type} is supported. */ + /** Returns whether the sample {@linkplain MimeTypes MIME type} is supported. */ public boolean supportsSampleMimeType(@Nullable String mimeType) { return muxerFactory.supportsSampleMimeType(mimeType, containerMimeType); } /** - * Returns the supported {@link MimeTypes MIME types} for the given {@link C.TrackType track - * type}. + * Returns the supported {@linkplain MimeTypes MIME types} for the given {@linkplain C.TrackType + * track type}. */ public ImmutableList getSupportedSampleMimeTypes(@C.TrackType int trackType) { return muxerFactory.getSupportedSampleMimeTypes(trackType, containerMimeType); @@ -98,9 +98,9 @@ import java.nio.ByteBuffer; /** * Adds a track format to the muxer. * - *

The tracks must all be {@link #registerTrack() registered} before any format is added and - * all the formats must be added before samples are {@link #writeSample(int, ByteBuffer, boolean, - * long) written}. + *

The tracks must all be {@linkplain #registerTrack() registered} before any format is added + * and all the formats must be added before samples are {@linkplain #writeSample(int, ByteBuffer, + * boolean, long) written}. * * @param format The {@link Format} to be added. * @throws IllegalStateException If the format is unsupported or if there is already a track @@ -133,16 +133,16 @@ import java.nio.ByteBuffer; /** * Attempts to write a sample to the muxer. * - * @param trackType The {@link C.TrackType track type} of the sample. + * @param trackType The {@linkplain C.TrackType track type} of the sample. * @param data The sample to write. * @param isKeyFrame Whether the sample is a key frame. * @param presentationTimeUs The presentation time of the sample in microseconds. * @return Whether the sample was successfully written. This is {@code false} if the muxer hasn't - * {@link #addTrackFormat(Format) received a format} for every {@link #registerTrack() - * registered track}, or if it should write samples of other track types first to ensure a - * good interleaving. - * @throws IllegalStateException If the muxer doesn't have any {@link #endTrack(int) non-ended} - * track of the given track type. + * {@linkplain #addTrackFormat(Format) received a format} for every {@linkplain + * #registerTrack() registered track}, or if it should write samples of other track types + * first to ensure a good interleaving. + * @throws IllegalStateException If the muxer doesn't have any {@linkplain #endTrack(int) + * non-ended} track of the given track type. * @throws Muxer.MuxerException If the underlying muxer fails to write the sample. */ public boolean writeSample( diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java b/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java index 58405ea29b..8602733a93 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/SefSlowMotionFlattener.java @@ -247,8 +247,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * output frame rate might be variable. * *

This method can only be called if all the frames until the current one (included) have been - * {@link #processCurrentFrame(int, long) processed} in order, and if the next frames have not - * been processed yet. + * {@linkplain #processCurrentFrame(int, long) processed} in order, and if the next frames have + * not been processed yet. */ @VisibleForTesting /* package */ long getCurrentFrameOutputTimeUs(long inputTimeUs) { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java index 8361d1d7d0..aad804edec 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationException.java @@ -147,7 +147,7 @@ public final class TransformationException extends Exception { /** * Caused by the output format for a track not being supported. * - *

Supported output formats are limited by the muxer's capabilities and the {@link + *

Supported output formats are limited by the muxer's capabilities and the {@linkplain * Codec.DecoderFactory encoders} available. */ public static final int ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED = 4003; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java index a0ca504c08..33a5ecb4c1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationRequest.java @@ -161,7 +161,7 @@ public final class TransformationRequest { * @param videoMimeType The MIME type of the video samples in the output. * @return This builder. * @throws IllegalArgumentException If the {@code videoMimeType} is non-null but not a video - * {@link MimeTypes MIME type}. + * {@linkplain MimeTypes MIME type}. */ public Builder setVideoMimeType(@Nullable String videoMimeType) { checkArgument( @@ -186,7 +186,7 @@ public final class TransformationRequest { * @param audioMimeType The MIME type of the audio samples in the output. * @return This builder. * @throws IllegalArgumentException If the {@code audioMimeType} is non-null but not an audio - * {@link MimeTypes MIME type}. + * {@linkplain MimeTypes MIME type}. */ public Builder setAudioMimeType(@Nullable String audioMimeType) { checkArgument( @@ -218,7 +218,7 @@ public final class TransformationRequest { * *

This method is experimental, and will be renamed or removed in a future release. The HDR * editing feature is under development and is intended for developing/testing HDR processing - * and encoding support. HDR editing can't be enabled at the same time as {@link + * and encoding support. HDR editing can't be enabled at the same time as {@linkplain * #setEnableRequestSdrToneMapping(boolean) SDR tone-mapping}. * * @param enableHdrEditing Whether to attempt to process any input video stream as a high @@ -278,15 +278,15 @@ public final class TransformationRequest { */ public final int outputHeight; /** - * The requested output audio sample {@link MimeTypes MIME type}, or {@code null} if inferred from - * the input. + * The requested output audio sample {@linkplain MimeTypes MIME type}, or {@code null} if inferred + * from the input. * * @see Builder#setAudioMimeType(String) */ @Nullable public final String audioMimeType; /** - * The requested output video sample {@link MimeTypes MIME type}, or {@code null} if inferred from - * the input. + * The requested output video sample {@linkplain MimeTypes MIME type}, or {@code null} if inferred + * from the input. * * @see Builder#setVideoMimeType(String) */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index f642399d17..cf59d3c23e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -208,7 +208,7 @@ public final class Transformer { * Sets the {@link MediaSource.Factory} to be used to retrieve the inputs to transform. * *

The default value is a {@link DefaultMediaSourceFactory} built with the context provided - * in {@link #Builder(Context) the constructor}. + * in {@linkplain #Builder(Context) the constructor}. * * @param mediaSourceFactory A {@link MediaSource.Factory}. * @return This builder. @@ -309,7 +309,7 @@ public final class Transformer { } /** - * Removes all {@link Transformer.Listener listeners}. + * Removes all {@linkplain Transformer.Listener listeners}. * *

This is equivalent to {@link Transformer#removeAllListeners()}. * @@ -639,7 +639,7 @@ public final class Transformer { } /** - * Removes all {@link Transformer.Listener listeners}. + * Removes all {@linkplain Transformer.Listener listeners}. * * @throws IllegalStateException If this method is called from the wrong thread. */ @@ -651,14 +651,14 @@ public final class Transformer { /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

The transformation state is notified through the {@link Builder#addListener(Listener) + *

The transformation state is notified through the {@linkplain Builder#addListener(Listener) * listener}. * *

Concurrent transformations on the same Transformer object are not allowed. * *

The output is an MP4 file. It can contain at most one video track and one audio track. Other - * track types are ignored. For adaptive bitrate {@link MediaSource media sources}, the highest - * bitrate video and audio streams are selected. + * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the + * highest bitrate video and audio streams are selected. * * @param mediaItem The {@link MediaItem} to transform. * @param path The path to the output file. @@ -674,14 +674,14 @@ public final class Transformer { /** * Starts an asynchronous operation to transform the given {@link MediaItem}. * - *

The transformation state is notified through the {@link Builder#addListener(Listener) + *

The transformation state is notified through the {@linkplain Builder#addListener(Listener) * listener}. * *

Concurrent transformations on the same Transformer object are not allowed. * *

The output is an MP4 file. It can contain at most one video track and one audio track. Other - * track types are ignored. For adaptive bitrate {@link MediaSource media sources}, the highest - * bitrate video and audio streams are selected. + * track types are ignored. For adaptive bitrate {@linkplain MediaSource media sources}, the + * highest bitrate video and audio streams are selected. * * @param mediaItem The {@link MediaItem} to transform. * @param parcelFileDescriptor A readable and writable {@link ParcelFileDescriptor} of the output. @@ -767,7 +767,7 @@ public final class Transformer { * Returns the current {@link ProgressState} and updates {@code progressHolder} with the current * progress if it is {@link #PROGRESS_STATE_AVAILABLE available}. * - *

After a transformation {@link Listener#onTransformationCompleted(MediaItem, + *

After a transformation {@linkplain Listener#onTransformationCompleted(MediaItem, * TransformationResult) completes}, this method returns {@link * #PROGRESS_STATE_NO_TRANSFORMATION}. * diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java index f00fc7f433..257795a52c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -181,7 +181,7 @@ public final class VideoEncoderSettings { /** The encoding bitrate. */ public final int bitrate; - /** One of {@link BitrateMode the allowed modes}. */ + /** One of {@linkplain BitrateMode the allowed modes}. */ public final @BitrateMode int bitrateMode; /** The encoding profile. */ public final int profile; From bd6eaab6b41e4b4446c6e48a521e4d9ed79d68f2 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 29 Mar 2022 13:45:59 +0100 Subject: [PATCH 019/116] Reword MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE javadoc. MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE was copied from a test class, but BitmapTestUtil isn't a test. So the javadoc needs rewording to reflect that. PiperOrigin-RevId: 438001833 --- .../media3/transformer/BitmapTestUtil.java | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java index db2df0c07e..a590b903f4 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/BitmapTestUtil.java @@ -64,18 +64,18 @@ public class BitmapTestUtil { public static final String ROTATE45_SCALE_TO_FIT_EXPECTED_OUTPUT_PNG_ASSET_STRING = "media/bitmap/sample_mp4_first_frame_rotate_45_scale_to_fit.png"; /** - * Maximum allowed average pixel difference between the expected and actual edited images for the - * test to pass. The value is chosen so that differences in decoder behavior across emulator - * versions don't affect whether the test passes for most emulators, but substantial distortions - * introduced by changes in the behavior of the {@link GlFrameProcessor GlFrameProcessors} will - * cause the test to fail. + * Maximum allowed average pixel difference between the expected and actual edited images in pixel + * difference-based tests. The value is chosen so that differences in decoder behavior across + * emulator versions don't affect whether the test passes for most emulators, but substantial + * distortions introduced by changes in the behavior of the {@link GlFrameProcessor + * GlFrameProcessors} will cause the test to fail. * - *

To run this test on physical devices, please use a value of 5f, rather than 0.1f. This - * higher value will ignore some very small errors, but will allow for some differences caused by - * graphics implementations to be ignored. When the difference is close to the threshold, manually - * inspect expected/actual bitmaps to confirm failure, as it's possible this is caused by a - * difference in the codec or graphics implementation as opposed to a {@link GlFrameProcessor} - * issue. + *

To run pixel difference-based tests on physical devices, please use a value of 5f, rather + * than 0.1f. This higher value will ignore some very small errors, but will allow for some + * differences caused by graphics implementations to be ignored. When the difference is close to + * the threshold, manually inspect expected/actual bitmaps to confirm failure, as it's possible + * this is caused by a difference in the codec or graphics implementation as opposed to a {@link + * GlFrameProcessor} issue. */ public static final float MAXIMUM_AVERAGE_PIXEL_ABSOLUTE_DIFFERENCE = 0.1f; From 2adf0f67d8f001b4c16f0ebc4c1c85f02719fd00 Mon Sep 17 00:00:00 2001 From: christosts Date: Tue, 29 Mar 2022 14:35:43 +0100 Subject: [PATCH 020/116] Fix NPE in PlayerActivity PiperOrigin-RevId: 438010395 --- .../main/java/androidx/media3/demo/main/PlayerActivity.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java index f556d8c8cb..c2ebd97480 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/PlayerActivity.java @@ -520,6 +520,9 @@ public class PlayerActivity extends AppCompatActivity @OptIn(markerClass = androidx.media3.common.util.UnstableApi.class) private static MediaItem maybeSetDownloadProperties( MediaItem item, @Nullable DownloadRequest downloadRequest) { + if (downloadRequest == null) { + return item; + } MediaItem.Builder builder = item.buildUpon(); builder .setMediaId(downloadRequest.id) From d97de5b9f0efd7c904874aa23157d29636ff33d1 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Tue, 29 Mar 2022 14:36:10 +0100 Subject: [PATCH 021/116] Use microseconds not nanoseconds for GlFrameProcessor. This requires an additional nanos to micros conversion because the SurfaceTexture uses nanos. But as the timestamps from the MediaCodec decoder (propagated in DefaultCodec#releaseOutputBuffer) are in microseconds no precision is lost here. Also add test that checks output video duration. PiperOrigin-RevId: 438010490 --- .../androidx/media3/common/util/Util.java | 19 +++++++++++++ .../androidx/media3/common/util/UtilTest.java | 16 +++++++++++ .../AdvancedFrameProcessorPixelTest.java | 8 +++--- .../TransformerAndroidTestRunner.java | 3 ++ .../transformer/TransformerEndToEndTest.java | 21 ++++++++++++++ .../transformer/AdvancedFrameProcessor.java | 2 +- .../ExternalCopyFrameProcessor.java | 2 +- .../transformer/FrameProcessorChain.java | 7 +++-- .../media3/transformer/GlFrameProcessor.java | 4 +-- .../media3/transformer/MuxerWrapper.java | 6 ++++ .../PresentationFrameProcessor.java | 4 +-- .../transformer/ScaleToFitFrameProcessor.java | 4 +-- .../transformer/TransformationResult.java | 28 ++++++++++++++++--- .../media3/transformer/Transformer.java | 1 + 14 files changed, 106 insertions(+), 19 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index 6cffae7fbd..a0619d1bfa 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1121,6 +1121,25 @@ public final class Util { return min; } + /** + * Returns the maximum value in the given {@link SparseLongArray}. + * + * @param sparseLongArray The {@link SparseLongArray}. + * @return The maximum value. + * @throws NoSuchElementException If the array is empty. + */ + @RequiresApi(18) + public static long maxValue(SparseLongArray sparseLongArray) { + if (sparseLongArray.size() == 0) { + throw new NoSuchElementException(); + } + long max = Long.MIN_VALUE; + for (int i = 0; i < sparseLongArray.size(); i++) { + max = max(max, sparseLongArray.valueAt(i)); + } + return max; + } + /** * Converts a time in microseconds to the corresponding time in milliseconds, preserving {@link * C#TIME_UNSET} and {@link C#TIME_END_OF_SOURCE} values. diff --git a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java index 3ffc4bfa4f..a2056e1542 100644 --- a/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/util/UtilTest.java @@ -21,6 +21,7 @@ import static androidx.media3.common.util.Util.escapeFileName; import static androidx.media3.common.util.Util.getCodecsOfType; import static androidx.media3.common.util.Util.getStringForTime; import static androidx.media3.common.util.Util.gzip; +import static androidx.media3.common.util.Util.maxValue; import static androidx.media3.common.util.Util.minValue; import static androidx.media3.common.util.Util.parseXsDateTime; import static androidx.media3.common.util.Util.parseXsDuration; @@ -747,6 +748,21 @@ public class UtilTest { assertThrows(NoSuchElementException.class, () -> minValue(new SparseLongArray())); } + @Test + public void sparseLongArrayMaxValue_returnsMaxValue() { + SparseLongArray sparseLongArray = new SparseLongArray(); + sparseLongArray.put(0, 2); + sparseLongArray.put(25, 10); + sparseLongArray.put(42, 1); + + assertThat(maxValue(sparseLongArray)).isEqualTo(10); + } + + @Test + public void sparseLongArrayMaxValue_emptyArray_throws() { + assertThrows(NoSuchElementException.class, () -> maxValue(new SparseLongArray())); + } + @Test public void parseXsDuration_returnsParsedDurationInMillis() { assertThat(parseXsDuration("PT150.279S")).isEqualTo(150279L); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java index 895d10af18..9fba1d9482 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java @@ -94,7 +94,7 @@ public final class AdvancedFrameProcessorPixelTest { advancedFrameProcessor.initialize(inputTexId); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); - advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); @@ -118,7 +118,7 @@ public final class AdvancedFrameProcessorPixelTest { Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); - advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); @@ -141,7 +141,7 @@ public final class AdvancedFrameProcessorPixelTest { Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); - advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); @@ -163,7 +163,7 @@ public final class AdvancedFrameProcessorPixelTest { advancedFrameProcessor.initialize(inputTexId); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); - advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeNs= */ 0); + advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); Bitmap actualBitmap = BitmapTestUtil.createArgb8888BitmapFromCurrentGlFramebuffer(width, height); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 499a015696..9b7a52625d 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -309,6 +309,9 @@ public class TransformerAndroidTestRunner { TransformationResult transformationResult = testResult.transformationResult; JSONObject transformationResultJson = new JSONObject(); + if (transformationResult.durationMs != C.LENGTH_UNSET) { + transformationResultJson.put("durationMs", transformationResult.durationMs); + } if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes); } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index d44f514b9d..cdf4e0d902 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -58,4 +58,25 @@ public class TransformerEndToEndTest { checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); } + + @Test + public void videoOnly_completesWithConsistentDuration() throws Exception { + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setTransformationRequest( + new TransformationRequest.Builder().setResolution(480).build()) + .setEncoderFactory( + new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) + .build(); + long expectedDurationMs = 967; + + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run(/* testId= */ "videoOnly_completesWithConsistentDuration", AVC_VIDEO_URI_STRING); + + assertThat(result.transformationResult.durationMs).isEqualTo(expectedDurationMs); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index dc9c73dd55..fb9b22519d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -124,7 +124,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { } @Override - public void updateProgramAndDraw(long presentationTimeNs) { + public void updateProgramAndDraw(long presentationTimeUs) { checkStateNotNull(glProgram); glProgram.use(); glProgram.bindAttributesAndUniforms(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 71bc45ea1d..502f1fb167 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -101,7 +101,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void updateProgramAndDraw(long presentationTimeNs) { + public void updateProgramAndDraw(long presentationTimeUs) { checkStateNotNull(glProgram); glProgram.use(); glProgram.bindAttributesAndUniforms(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 8346bde888..9645a1d52a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -412,7 +412,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix); long presentationTimeNs = inputSurfaceTexture.getTimestamp(); - externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeNs); + long presentationTimeUs = presentationTimeNs / 1000; + externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs); for (int i = 0; i < frameProcessors.size() - 1; i++) { Size outputSize = inputSizes.get(i + 1); @@ -423,11 +424,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; framebuffers[i + 1], outputSize.getWidth(), outputSize.getHeight()); - frameProcessors.get(i).updateProgramAndDraw(presentationTimeNs); + frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); } if (!frameProcessors.isEmpty()) { GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); - getLast(frameProcessors).updateProgramAndDraw(presentationTimeNs); + getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); } EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index 84477c06e6..ad9099d8f1 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -60,9 +60,9 @@ public interface GlFrameProcessor { *

The frame processor must be {@linkplain #initialize(int) initialized}. The caller is * responsible for focussing the correct render target before calling this method. * - * @param presentationTimeNs The presentation timestamp of the current frame, in nanoseconds. + * @param presentationTimeUs The presentation timestamp of the current frame, in microseconds. */ - void updateProgramAndDraw(long presentationTimeNs); + void updateProgramAndDraw(long presentationTimeUs); /** Releases all resources. */ void release(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index 5babefd873..2a0a946910 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -17,6 +17,7 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkState; +import static androidx.media3.common.util.Util.maxValue; import static androidx.media3.common.util.Util.minValue; import android.util.SparseIntArray; @@ -240,4 +241,9 @@ import java.nio.ByteBuffer; } return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; } + + /** Returns the duration of the longest track in milliseconds. */ + public long getDurationMs() { + return Util.usToMs(maxValue(trackTypeToTimeUs)); + } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java index f6941f2e41..8b0bf17c8d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java @@ -152,8 +152,8 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { } @Override - public void updateProgramAndDraw(long presentationTimeNs) { - checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs); + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java index 0875e706c4..22a59a5156 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java @@ -176,8 +176,8 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { } @Override - public void updateProgramAndDraw(long presentationTimeNs) { - checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeNs); + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java index b5ece274a6..2bf98abe1d 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java @@ -27,16 +27,29 @@ public final class TransformationResult { /** A builder for {@link TransformationResult} instances. */ public static final class Builder { + private long durationMs; private long fileSizeBytes; private int averageAudioBitrate; private int averageVideoBitrate; public Builder() { + durationMs = C.TIME_UNSET; fileSizeBytes = C.LENGTH_UNSET; averageAudioBitrate = C.RATE_UNSET_INT; averageVideoBitrate = C.RATE_UNSET_INT; } + /** + * Sets the duration of the video in milliseconds. + * + *

Input must be positive or {@link C#TIME_UNSET}. + */ + public Builder setDurationMs(long durationMs) { + checkArgument(durationMs > 0 || durationMs == C.TIME_UNSET); + this.durationMs = durationMs; + return this; + } + /** * Sets the file size in bytes. * @@ -71,10 +84,13 @@ public final class TransformationResult { } public TransformationResult build() { - return new TransformationResult(fileSizeBytes, averageAudioBitrate, averageVideoBitrate); + return new TransformationResult( + durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate); } } + /** The duration of the video in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ + public final long durationMs; /** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */ public final long fileSizeBytes; /** @@ -87,7 +103,8 @@ public final class TransformationResult { public final int averageVideoBitrate; private TransformationResult( - long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { + long durationMs, long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { + this.durationMs = durationMs; this.fileSizeBytes = fileSizeBytes; this.averageAudioBitrate = averageAudioBitrate; this.averageVideoBitrate = averageVideoBitrate; @@ -95,6 +112,7 @@ public final class TransformationResult { public Builder buildUpon() { return new Builder() + .setDurationMs(durationMs) .setFileSizeBytes(fileSizeBytes) .setAverageAudioBitrate(averageAudioBitrate) .setAverageVideoBitrate(averageVideoBitrate); @@ -109,14 +127,16 @@ public final class TransformationResult { return false; } TransformationResult result = (TransformationResult) o; - return fileSizeBytes == result.fileSizeBytes + return durationMs == result.durationMs + && fileSizeBytes == result.fileSizeBytes && averageAudioBitrate == result.averageAudioBitrate && averageVideoBitrate == result.averageVideoBitrate; } @Override public int hashCode() { - int result = (int) fileSizeBytes; + int result = (int) durationMs; + result = 31 * result + (int) fileSizeBytes; result = 31 * result + averageAudioBitrate; result = 31 * result + averageVideoBitrate; return result; diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index cf59d3c23e..28e7c4f5c3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -1002,6 +1002,7 @@ public final class Transformer { } else { TransformationResult result = new TransformationResult.Builder() + .setDurationMs(muxerWrapper.getDurationMs()) .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO)) .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO)) .build(); From 8c0e8898fb06689cae64b7b09d052a576a88ccc9 Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 29 Mar 2022 16:08:50 +0100 Subject: [PATCH 022/116] Publish session test modules for the media3 repo Issue: androidx/media#60 PiperOrigin-RevId: 438028148 --- core_settings.gradle | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/core_settings.gradle b/core_settings.gradle index d8760a1709..baca421753 100644 --- a/core_settings.gradle +++ b/core_settings.gradle @@ -87,3 +87,7 @@ include modulePrefix + 'test-data' project(modulePrefix + 'test-data').projectDir = new File(rootDir, 'libraries/test_data') include modulePrefix + 'test-utils' project(modulePrefix + 'test-utils').projectDir = new File(rootDir, 'libraries/test_utils') +include modulePrefix + 'test-session-common' +project(modulePrefix + 'test-session-common').projectDir = new File(rootDir, 'libraries/test_session_common') +include modulePrefix + 'test-session-current' +project(modulePrefix + 'test-session-current').projectDir = new File(rootDir, 'libraries/test_session_current') From e699765df5d1390c6c680eb5955e562eaf884825 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Tue, 29 Mar 2022 16:19:48 +0100 Subject: [PATCH 023/116] Add support for analyzing bitrates across devices. Allows for input values to be propagated to the analysis file. PiperOrigin-RevId: 438030322 --- .../TransformerAndroidTestRunner.java | 32 ++++- .../mh/analysis/BitrateAnalysisTest.java | 118 ++++++++++++++++++ .../transformer/mh/analysis/package-info.java | 19 +++ 3 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java create mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 9b7a52625d..9211149761 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -30,6 +30,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; @@ -51,6 +52,7 @@ public class TransformerAndroidTestRunner { private boolean calculateSsim; private int timeoutSeconds; private boolean suppressAnalysisExceptions; + @Nullable private Map inputValues; /** * Creates a {@link Builder}. @@ -114,10 +116,30 @@ public class TransformerAndroidTestRunner { return this; } + /** + * Sets a {@link Map} of transformer input values, which are propagated to the transformation + * summary JSON file. + * + *

Values in the map should be convertible according to {@link JSONObject#wrap(Object)} to be + * recorded properly in the summary file. + * + * @param inputValues A {@link Map} of values to be written to the transformation summary. + * @return This {@link Builder}. + */ + public Builder setInputValues(@Nullable Map inputValues) { + this.inputValues = inputValues; + return this; + } + /** Builds the {@link TransformerAndroidTestRunner}. */ public TransformerAndroidTestRunner build() { return new TransformerAndroidTestRunner( - context, transformer, timeoutSeconds, calculateSsim, suppressAnalysisExceptions); + context, + transformer, + timeoutSeconds, + calculateSsim, + suppressAnalysisExceptions, + inputValues); } } @@ -126,31 +148,35 @@ public class TransformerAndroidTestRunner { private final int timeoutSeconds; private final boolean calculateSsim; private final boolean suppressAnalysisExceptions; + @Nullable private final Map inputValues; private TransformerAndroidTestRunner( Context context, Transformer transformer, int timeoutSeconds, boolean calculateSsim, - boolean suppressAnalysisExceptions) { + boolean suppressAnalysisExceptions, + @Nullable Map inputValues) { this.context = context; this.transformer = transformer; this.timeoutSeconds = timeoutSeconds; this.calculateSsim = calculateSsim; this.suppressAnalysisExceptions = suppressAnalysisExceptions; + this.inputValues = inputValues; } /** * Transforms the {@code uriString}, saving a summary of the transformation to the application * cache. * - * @param testId An identifier for the test. + * @param testId A unique identifier for the transformer test run. * @param uriString The uri (as a {@link String}) of the file to transform. * @return The {@link TransformationTestResult}. * @throws Exception The cause of the transformation not completing. */ public TransformationTestResult run(String testId, String uriString) throws Exception { JSONObject resultJson = new JSONObject(); + resultJson.put("inputValues", JSONObject.wrap(inputValues)); try { TransformationTestResult transformationTestResult = runInternal(testId, uriString); resultJson.put("transformationResult", getTestResultJson(transformationTestResult)); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java new file mode 100644 index 0000000000..c6fd8e09f3 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/BitrateAnalysisTest.java @@ -0,0 +1,118 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer.mh.analysis; + +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_CBR; +import static android.media.MediaCodecInfo.EncoderCapabilities.BITRATE_MODE_VBR; + +import android.content.Context; +import androidx.media3.common.util.Assertions; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.Iterables; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** Instrumentation tests for analysing output bitrate and quality for a given input bitrate. */ +@RunWith(Parameterized.class) +public class BitrateAnalysisTest { + private static final ImmutableList INPUT_FILES = + ImmutableList.of( + AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING, + AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING); + private static final ImmutableList INPUT_BITRATE_MODES = + ImmutableList.of(BITRATE_MODE_VBR, BITRATE_MODE_CBR); + + private static final int START_BITRATE = 2_000_000; + private static final int END_BITRATE = 10_000_000; + private static final int BITRATE_INTERVAL = 1_000_000; + + @Parameter(0) + public int bitrate; + + @Parameter(1) + public int bitrateMode; + + @Parameter(2) + public @MonotonicNonNull String fileUri; + + @Parameters(name = "analyzeBitrate_{0}_{1}_{2}") + public static List parameters() { + List parameterList = new ArrayList<>(); + for (int bitrate = START_BITRATE; bitrate <= END_BITRATE; bitrate += BITRATE_INTERVAL) { + for (int mode : INPUT_BITRATE_MODES) { + for (String file : INPUT_FILES) { + parameterList.add(new Object[] {bitrate, mode, file}); + } + } + } + + return parameterList; + } + + @Test + public void analyzeBitrate() throws Exception { + Assertions.checkNotNull(fileUri); + String fileName = Assertions.checkNotNull(Iterables.getLast(Splitter.on("/").split(fileUri))); + String testId = String.format("analyzeBitrate_ssim_%s_%d_%s", bitrate, bitrateMode, fileName); + + Map inputValues = new HashMap<>(); + inputValues.put("targetBitrate", bitrate); + inputValues.put("inputFilename", fileName); + if (bitrateMode == BITRATE_MODE_CBR) { + inputValues.put("bitrateMode", "CBR"); + } else if (bitrateMode == BITRATE_MODE_VBR) { + inputValues.put("bitrateMode", "VBR"); + } + + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + new VideoEncoderSettings.Builder() + .setBitrate(bitrate) + .setBitrateMode(bitrateMode) + .build(), + /* enableFallback= */ false)) + .build(); + + inputValues.put("Transformer", transformer); + + new TransformerAndroidTestRunner.Builder(context, transformer) + .setInputValues(inputValues) + .setCalculateSsim(true) + .build() + .run(testId, fileUri); + } +} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java new file mode 100644 index 0000000000..f624be94c1 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/package-info.java @@ -0,0 +1,19 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@NonNullApi +package androidx.media3.transformer.mh.analysis; + +import androidx.media3.common.util.NonNullApi; From 3ac7e0e84eaa9aea1541ae4bfc85380135a93f7c Mon Sep 17 00:00:00 2001 From: bachinger Date: Tue, 29 Mar 2022 17:00:52 +0100 Subject: [PATCH 024/116] Update error state of legacy playback state if authentication fails This change adds the ability to update the error code of the PlaybackStateCompat in cases we need this for backwards compatibility. It is applied in the least intrusive way because normally, return values of a service method should not change the state of the `PlaybackStateCompat`, just because it has nothing to do with the playback state but rather with the state of the `MediaLibrarySession`. For this reason only the error code `RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED` is taken into account while all other error codes are not mapped to the `PlaybackStateCompat'. PiperOrigin-RevId: 438038852 --- .../media3/session/LibraryResult.java | 17 ++- .../media3/session/MediaConstants.java | 27 +++++ .../session/MediaLibrarySessionImpl.java | 100 ++++++++++++------ .../media3/session/PlayerWrapper.java | 54 ++++++++++ .../session/src/main/res/values/strings.xml | 1 + .../session/common/MediaBrowserConstants.java | 3 + ...wserCompatWithMediaLibraryServiceTest.java | 44 ++++++++ ...wserCompatWithMediaSessionServiceTest.java | 25 +++++ .../session/MockMediaLibraryService.java | 21 ++++ 9 files changed, 256 insertions(+), 36 deletions(-) diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index 17c921eff4..f602e35379 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -218,11 +218,24 @@ public final class LibraryResult implements Bundleable { * @param errorCode The error code. */ public static LibraryResult ofError(@Code int errorCode) { + return ofError(errorCode, /* params= */ null); + } + + /** + * Creates an instance with an unsuccessful {@link Code result code} and {@link LibraryParams} to + * describe the error. + * + *

{@code errorCode} must not be {@link #RESULT_SUCCESS}. + * + * @param errorCode The error code. + * @param params The optional parameters to describe the error. + */ + public static LibraryResult ofError(@Code int errorCode, @Nullable LibraryParams params) { checkArgument(errorCode != RESULT_SUCCESS); return new LibraryResult<>( - errorCode, + /* resultCode= */ errorCode, SystemClock.elapsedRealtime(), - /* params= */ null, + /* params= */ params, /* value= */ null, VALUE_TYPE_ERROR); } diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java index 217757283d..9ad21b8784 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaConstants.java @@ -113,6 +113,33 @@ public final class MediaConstants { */ public static final String MEDIA_URI_QUERY_URI = "uri"; + /** + * The extras key for the localized error resolution string. + * + *

See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_LABEL"; + /** + * The extras key for the error resolution intent. + * + *

See {@link + * androidx.media.utils.MediaConstants#PLAYBACK_STATE_EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT}. + */ + public static final String EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT = + "android.media.extras.ERROR_RESOLUTION_ACTION_INTENT"; + + /** The legacy status code for successful execution. */ + public static final int STATUS_CODE_SUCCESS_COMPAT = -1; + + /** + * The legacy error code for expired authentication. + * + *

See {@code PlaybackStateCompat#ERROR_CODE_AUTHENTICATION_EXPIRED}. + */ + public static final int ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT = 3; + /* package */ static final String SESSION_COMMAND_ON_EXTRAS_CHANGED = "androidx.media3.session.SESSION_COMMAND_ON_EXTRAS_CHANGED"; /* package */ static final String SESSION_COMMAND_ON_CAPTIONING_ENABLED_CHANGED = diff --git a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java index f33347f961..5faa6bdc12 100644 --- a/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java +++ b/libraries/session/src/main/java/androidx/media3/session/MediaLibrarySessionImpl.java @@ -18,7 +18,10 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkStateNotNull; +import static androidx.media3.session.LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED; import static androidx.media3.session.LibraryResult.RESULT_SUCCESS; +import static androidx.media3.session.MediaConstants.ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; import android.app.PendingIntent; import android.content.Context; @@ -118,19 +121,17 @@ import java.util.concurrent.Future; public ListenableFuture> onGetLibraryRootOnHandler( ControllerInfo browser, @Nullable LibraryParams params) { - // onGetLibraryRoot is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetLibraryRoot(instance, browser, params), - "onGetLibraryRoot must return non-null future"); - } - - public ListenableFuture> onGetItemOnHandler( - ControllerInfo browser, String mediaId) { - // onGetItem is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onGetItem(instance, browser, mediaId), "onGetItem must return non-null future"); + ListenableFuture> future = + callback.onGetLibraryRoot(instance, browser, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetChildrenOnHandler( @@ -139,16 +140,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetChildren is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetChildren(instance, browser, parentId, page, pageSize, params), - "onGetChildren must return non-null future"); + callback.onGetChildren(instance, browser, parentId, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -156,6 +154,21 @@ import java.util.concurrent.Future; return future; } + public ListenableFuture> onGetItemOnHandler( + ControllerInfo browser, String mediaId) { + ListenableFuture> future = + callback.onGetItem(instance, browser, mediaId); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; + } + public ListenableFuture> onSubscribeOnHandler( ControllerInfo browser, String parentId, @Nullable LibraryParams params) { ControllerCb controller = checkStateNotNull(browser.getControllerCb()); @@ -193,12 +206,8 @@ import java.util.concurrent.Future; public ListenableFuture> onUnsubscribeOnHandler( ControllerInfo browser, String parentId) { - // onUnsubscribe is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. ListenableFuture> future = - checkNotNull( - callback.onUnsubscribe(instance, browser, parentId), - "onUnsubscribe must return non-null future"); + callback.onUnsubscribe(instance, browser, parentId); future.addListener( () -> removeSubscription(checkStateNotNull(browser.getControllerCb()), parentId), @@ -209,11 +218,17 @@ import java.util.concurrent.Future; public ListenableFuture> onSearchOnHandler( ControllerInfo browser, String query, @Nullable LibraryParams params) { - // onSearch is defined to return a non-null result but it's implemented by applications, - // so we explicitly null-check the result to fail early if an app accidentally returns null. - return checkNotNull( - callback.onSearch(instance, browser, query, params), - "onSearch must return non-null future"); + ListenableFuture> future = + callback.onSearch(instance, browser, query, params); + future.addListener( + () -> { + @Nullable LibraryResult result = tryGetFutureResult(future); + if (result != null) { + maybeUpdateLegacyErrorState(result); + } + }, + MoreExecutors.directExecutor()); + return future; } public ListenableFuture>> onGetSearchResultOnHandler( @@ -222,17 +237,13 @@ import java.util.concurrent.Future; int page, int pageSize, @Nullable LibraryParams params) { - // onGetSearchResult is defined to return a non-null result but it's implemented by - // applications, so we explicitly null-check the result to fail early if an app accidentally - // returns null. ListenableFuture>> future = - checkNotNull( - callback.onGetSearchResult(instance, browser, query, page, pageSize, params), - "onGetSearchResult must return non-null future"); + callback.onGetSearchResult(instance, browser, query, page, pageSize, params); future.addListener( () -> { @Nullable LibraryResult> result = tryGetFutureResult(future); if (result != null) { + maybeUpdateLegacyErrorState(result); verifyResultItems(result, pageSize); } }, @@ -277,6 +288,27 @@ import java.util.concurrent.Future; return true; } + private void maybeUpdateLegacyErrorState(LibraryResult result) { + PlayerWrapper playerWrapper = getPlayerWrapper(); + if (result.resultCode == RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED + && result.params != null + && result.params.extras.containsKey(EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT)) { + // Mapping this error to the legacy error state provides backwards compatibility for the + // Automotive OS sign-in. + MediaSessionCompat mediaSessionCompat = getSessionCompat(); + if (playerWrapper.getLegacyStatusCode() != RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED) { + playerWrapper.setLegacyErrorStatus( + ERROR_CODE_AUTHENTICATION_EXPIRED_COMPAT, + getContext().getString(R.string.authentication_required), + result.params.extras); + mediaSessionCompat.setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } else if (playerWrapper.getLegacyStatusCode() != RESULT_SUCCESS) { + playerWrapper.clearLegacyErrorStatus(); + getSessionCompat().setPlaybackState(playerWrapper.createPlaybackStateCompat()); + } + } + @Nullable private static T tryGetFutureResult(Future future) { checkState(future.isDone()); diff --git a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java index cdac091033..36fd98bfa2 100644 --- a/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java +++ b/libraries/session/src/main/java/androidx/media3/session/PlayerWrapper.java @@ -15,10 +15,13 @@ */ package androidx.media3.session; +import static androidx.media3.common.util.Assertions.checkNotNull; import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Util.postOrRun; +import static androidx.media3.session.MediaConstants.STATUS_CODE_SUCCESS_COMPAT; import android.media.AudioManager; +import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; @@ -52,8 +55,46 @@ import java.util.List; */ /* package */ class PlayerWrapper extends ForwardingPlayer { + private int legacyStatusCode; + @Nullable private String legacyErrorMessage; + @Nullable private Bundle legacyErrorExtras; + public PlayerWrapper(Player player) { super(player); + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + } + + /** + * Sets the legacy error code. + * + *

This sets the legacy {@link PlaybackStateCompat} to {@link PlaybackStateCompat#STATE_ERROR} + * and calls {@link PlaybackStateCompat.Builder#setErrorMessage(int, CharSequence)} and {@link + * PlaybackStateCompat.Builder#setExtras(Bundle)} with the given arguments. + * + *

Use {@link #clearLegacyErrorStatus()} to clear the error state and to resume to the actual + * playback state reflecting the player. + * + * @param errorCode The legacy error code. + * @param errorMessage The legacy error message. + * @param extras The extras. + */ + public void setLegacyErrorStatus(int errorCode, String errorMessage, Bundle extras) { + checkState(errorCode != STATUS_CODE_SUCCESS_COMPAT); + legacyStatusCode = errorCode; + legacyErrorMessage = errorMessage; + legacyErrorExtras = extras; + } + + /** Returns the legacy status code. */ + public int getLegacyStatusCode() { + return legacyStatusCode; + } + + /** Clears the legacy error status. */ + public void clearLegacyErrorStatus() { + legacyStatusCode = STATUS_CODE_SUCCESS_COMPAT; + legacyErrorMessage = null; + legacyErrorExtras = null; } @Override @@ -702,6 +743,19 @@ import java.util.List; } public PlaybackStateCompat createPlaybackStateCompat() { + if (legacyStatusCode != STATUS_CODE_SUCCESS_COMPAT) { + return new PlaybackStateCompat.Builder() + .setState( + PlaybackStateCompat.STATE_ERROR, + /* position= */ PlaybackStateCompat.PLAYBACK_POSITION_UNKNOWN, + /* playbackSpeed= */ 0, + /* updateTime= */ SystemClock.elapsedRealtime()) + .setActions(0) + .setBufferedPosition(0) + .setErrorMessage(legacyStatusCode, checkNotNull(legacyErrorMessage)) + .setExtras(checkNotNull(legacyErrorExtras)) + .build(); + } @Nullable PlaybackException playerError = getPlayerError(); int state = MediaUtils.convertToPlaybackStateCompatState( diff --git a/libraries/session/src/main/res/values/strings.xml b/libraries/session/src/main/res/values/strings.xml index 4b5b6c86e0..06eef42afe 100644 --- a/libraries/session/src/main/res/values/strings.xml +++ b/libraries/session/src/main/res/values/strings.xml @@ -28,4 +28,5 @@ Seek back Seek forward + Authentication required diff --git a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java index 6a132efbc1..b20ec27ab2 100644 --- a/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java +++ b/libraries/test_session_common/src/main/java/androidx/media3/test/session/common/MediaBrowserConstants.java @@ -36,6 +36,9 @@ public class MediaBrowserConstants { public static final String PARENT_ID_LONG_LIST = "parent_id_long_list"; public static final String PARENT_ID_NO_CHILDREN = "parent_id_no_children"; public static final String PARENT_ID_ERROR = "parent_id_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR = "parent_auth_expired_error"; + public static final String PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL = + "parent_auth_expired_error_label"; public static final List GET_CHILDREN_RESULT = new ArrayList<>(); public static final int CHILDREN_COUNT = 100; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java index 7bb315e509..aadc857a3c 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaLibraryServiceTest.java @@ -32,6 +32,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_ITEM_WITH_METADATA; import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID_GET_PLAYABLE_ITEM; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_NO_CHILDREN; @@ -59,6 +61,7 @@ import android.support.v4.media.MediaBrowserCompat.MediaItem; import android.support.v4.media.MediaBrowserCompat.SearchCallback; import android.support.v4.media.MediaBrowserCompat.SubscriptionCallback; import android.support.v4.media.MediaDescriptionCompat; +import android.support.v4.media.session.PlaybackStateCompat; import android.text.TextUtils; import androidx.media3.test.session.common.TestUtils; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -301,6 +304,47 @@ public class MediaBrowserCompatWithMediaLibraryServiceTest assertThat(latch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); } + @Test + public void getChildren_authErrorResult() throws InterruptedException { + String testParentId = PARENT_ID_AUTH_EXPIRED_ERROR; + connectAndWait(); + CountDownLatch errorLatch = new CountDownLatch(1); + browserCompat.subscribe( + testParentId, + new SubscriptionCallback() { + @Override + public void onError(String parentId) { + assertThat(parentId).isEqualTo(testParentId); + errorLatch.countDown(); + } + }); + assertThat(errorLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + assertThat(lastReportedPlaybackStateCompat).isNotNull(); + assertThat(lastReportedPlaybackStateCompat.getState()) + .isEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat( + lastReportedPlaybackStateCompat + .getExtras() + .getString(MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT)) + .isEqualTo(PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + + CountDownLatch successLatch = new CountDownLatch(1); + browserCompat.subscribe( + PARENT_ID, + new SubscriptionCallback() { + @Override + public void onChildrenLoaded(String parentId, List children) { + assertThat(parentId).isEqualTo(PARENT_ID); + successLatch.countDown(); + } + }); + assertThat(successLatch.await(TIMEOUT_MS, MILLISECONDS)).isTrue(); + // Any successful calls remove the error state, + assertThat(lastReportedPlaybackStateCompat.getState()) + .isNotEqualTo(PlaybackStateCompat.STATE_ERROR); + assertThat(lastReportedPlaybackStateCompat.getExtras()).isNull(); + } + @Test public void getChildren_emptyResult() throws InterruptedException { String testParentId = PARENT_ID_NO_CHILDREN; diff --git a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java index 18315ca674..64db577d13 100644 --- a/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java +++ b/libraries/test_session_current/src/androidTest/java/androidx/media3/session/MediaBrowserCompatWithMediaSessionServiceTest.java @@ -25,6 +25,8 @@ import android.content.ComponentName; import android.content.Context; import android.support.v4.media.MediaBrowserCompat; import android.support.v4.media.session.MediaControllerCompat; +import android.support.v4.media.session.PlaybackStateCompat; +import androidx.annotation.Nullable; import androidx.media3.test.session.common.HandlerThreadTestRule; import androidx.media3.test.session.common.TestHandler; import androidx.test.core.app.ApplicationProvider; @@ -56,7 +58,9 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { Context context; TestHandler handler; MediaBrowserCompat browserCompat; + @Nullable MediaControllerCompat controllerCompat; TestConnectionCallback connectionCallback; + @Nullable PlaybackStateCompat lastReportedPlaybackStateCompat; @Before public void setUp() throws Exception { @@ -117,6 +121,8 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { public final CountDownLatch suspendedLatch = new CountDownLatch(1); public final CountDownLatch failedLatch = new CountDownLatch(1); + @Nullable MediaControllerCompat.Callback controllerCompatCallback; + TestConnectionCallback() { super(); } @@ -124,19 +130,38 @@ public class MediaBrowserCompatWithMediaSessionServiceTest { @Override public void onConnected() { super.onConnected(); + // Make browser's internal handler to be initialized with test thread. + controllerCompat = new MediaControllerCompat(context, browserCompat.getSessionToken()); + controllerCompatCallback = + new MediaControllerCompat.Callback() { + @Override + public void onPlaybackStateChanged(PlaybackStateCompat state) { + lastReportedPlaybackStateCompat = state; + } + }; + controllerCompat.registerCallback(controllerCompatCallback); connectedLatch.countDown(); } @Override public void onConnectionSuspended() { super.onConnectionSuspended(); + unregisterControllerCallback(); suspendedLatch.countDown(); } @Override public void onConnectionFailed() { super.onConnectionFailed(); + unregisterControllerCallback(); failedLatch.countDown(); } + + private void unregisterControllerCallback() { + if (controllerCompat != null && controllerCompatCallback != null) { + controllerCompat.unregisterCallback(controllerCompatCallback); + } + controllerCompatCallback = null; + } } } diff --git a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java index 5b08c544bb..49550bbb05 100644 --- a/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java +++ b/libraries/test_session_current/src/main/java/androidx/media3/session/MockMediaLibraryService.java @@ -16,6 +16,8 @@ package androidx.media3.session; import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT; +import static androidx.media3.session.MediaConstants.EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT; import static androidx.media3.session.MediaTestUtils.assertLibraryParamsEquals; import static androidx.media3.test.session.common.CommonConstants.SUPPORT_APP_PACKAGE_NAME; import static androidx.media3.test.session.common.MediaBrowserConstants.CUSTOM_ACTION; @@ -30,6 +32,8 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.MEDIA_ID import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_EXTRAS; import static androidx.media3.test.session.common.MediaBrowserConstants.NOTIFY_CHILDREN_CHANGED_ITEM_COUNT; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR; +import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_ERROR; import static androidx.media3.test.session.common.MediaBrowserConstants.PARENT_ID_LONG_LIST; import static androidx.media3.test.session.common.MediaBrowserConstants.ROOT_EXTRAS; @@ -47,8 +51,10 @@ import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIB import static androidx.media3.test.session.common.MediaBrowserConstants.SUBSCRIBE_ID_NOTIFY_CHILDREN_CHANGED_TO_ONE_WITH_NON_SUBSCRIBED_ID; import static java.util.concurrent.TimeUnit.MILLISECONDS; +import android.app.PendingIntent; import android.app.Service; import android.content.Context; +import android.content.Intent; import android.os.Bundle; import android.os.HandlerThread; import androidx.annotation.GuardedBy; @@ -232,6 +238,21 @@ public class MockMediaLibraryService extends MediaLibraryService { return Futures.immediateFuture(LibraryResult.ofItemList(list, params)); } else if (PARENT_ID_ERROR.equals(parentId)) { return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_BAD_VALUE)); + } else if (PARENT_ID_AUTH_EXPIRED_ERROR.equals(parentId)) { + Bundle bundle = new Bundle(); + Intent signInIntent = new Intent("action"); + int flags = Util.SDK_INT >= 23 ? PendingIntent.FLAG_IMMUTABLE : 0; + bundle.putParcelable( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_INTENT_COMPAT, + PendingIntent.getActivity( + getApplicationContext(), /* requestCode= */ 0, signInIntent, flags)); + bundle.putString( + EXTRAS_KEY_ERROR_RESOLUTION_ACTION_LABEL_COMPAT, + PARENT_ID_AUTH_EXPIRED_ERROR_KEY_ERROR_RESOLUTION_ACTION_LABEL); + return Futures.immediateFuture( + LibraryResult.ofError( + LibraryResult.RESULT_ERROR_SESSION_AUTHENTICATION_EXPIRED, + new LibraryParams.Builder().setExtras(bundle).build())); } // Includes the case of PARENT_ID_NO_CHILDREN. return Futures.immediateFuture(LibraryResult.ofItemList(ImmutableList.of(), params)); From 01c24e4de88382fc2bb86cf211f030fc57463d99 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Wed, 30 Mar 2022 11:04:08 +0100 Subject: [PATCH 025/116] Move Json helper methods to AndroidTestUtil. PiperOrigin-RevId: 438253138 --- .../media3/transformer/AndroidTestUtil.java | 32 +++++++++++ .../transformer/TransformationTestResult.java | 31 +++++++++- .../TransformerAndroidTestRunner.java | 57 +------------------ 3 files changed, 65 insertions(+), 55 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 9560be07b7..5740f31cb7 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -18,8 +18,12 @@ package androidx.media3.transformer; import static androidx.media3.common.util.Assertions.checkState; import android.content.Context; +import android.os.Build; +import androidx.media3.common.util.Log; import java.io.File; import java.io.IOException; +import org.json.JSONException; +import org.json.JSONObject; /** Utilities for instrumentation tests. */ public final class AndroidTestUtil { @@ -44,5 +48,33 @@ public final class AndroidTestUtil { return file; } + /** + * Returns a {@link JSONObject} containing device specific details from {@link Build}, including + * manufacturer, model, SDK version and build fingerprint. + */ + public static JSONObject getDeviceDetailsAsJsonObject() throws JSONException { + return new JSONObject() + .put("manufacturer", Build.MANUFACTURER) + .put("model", Build.MODEL) + .put("sdkVersion", Build.VERSION.SDK_INT) + .put("fingerprint", Build.FINGERPRINT); + } + + /** + * Converts an exception to a {@link JSONObject}. + * + *

If the exception is a {@link TransformationException}, {@code errorCode} is included. + */ + public static JSONObject exceptionAsJsonObject(Exception exception) throws JSONException { + JSONObject exceptionJson = new JSONObject(); + exceptionJson.put("message", exception.getMessage()); + exceptionJson.put("type", exception.getClass()); + if (exception instanceof TransformationException) { + exceptionJson.put("errorCode", ((TransformationException) exception).errorCode); + } + exceptionJson.put("stackTrace", Log.getThrowableString(exception)); + return exceptionJson; + } + private AndroidTestUtil() {} } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java index 06cd6f27ce..d74e5484d9 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java @@ -17,6 +17,8 @@ package androidx.media3.transformer; import androidx.annotation.Nullable; import androidx.media3.common.C; +import org.json.JSONException; +import org.json.JSONObject; /** A test only class for holding the details of a test transformation. */ public class TransformationTestResult { @@ -111,11 +113,38 @@ public class TransformationTestResult { /** The SSIM score of the transformation, {@link #SSIM_UNSET} if unavailable. */ public final double ssim; /** - * The {@link Exception} that was thrown during post-tranformation analysis, or {@code null} if + * The {@link Exception} that was thrown during post-transformation analysis, or {@code null} if * nothing was thrown. */ @Nullable public final Exception analysisException; + /** Returns a {@link JSONObject} representing all the values in {@code this}. */ + public JSONObject asJsonObject() throws JSONException { + JSONObject jsonObject = new JSONObject(); + if (transformationResult.durationMs != C.LENGTH_UNSET) { + jsonObject.put("durationMs", transformationResult.durationMs); + } + if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { + jsonObject.put("fileSizeBytes", transformationResult.fileSizeBytes); + } + if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) { + jsonObject.put("averageAudioBitrate", transformationResult.averageAudioBitrate); + } + if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { + jsonObject.put("averageVideoBitrate", transformationResult.averageVideoBitrate); + } + if (elapsedTimeMs != C.TIME_UNSET) { + jsonObject.put("elapsedTimeMs", elapsedTimeMs); + } + if (ssim != TransformationTestResult.SSIM_UNSET) { + jsonObject.put("ssim", ssim); + } + if (analysisException != null) { + jsonObject.put("analysisException", AndroidTestUtil.exceptionAsJsonObject(analysisException)); + } + return jsonObject; + } + private TransformationTestResult( TransformationResult transformationResult, @Nullable String filePath, diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 9211149761..043eaa2b84 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -20,9 +20,7 @@ import static java.util.concurrent.TimeUnit.SECONDS; import android.content.Context; import android.net.Uri; -import android.os.Build; import androidx.annotation.Nullable; -import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.util.Log; import androidx.media3.common.util.SystemClock; @@ -179,13 +177,13 @@ public class TransformerAndroidTestRunner { resultJson.put("inputValues", JSONObject.wrap(inputValues)); try { TransformationTestResult transformationTestResult = runInternal(testId, uriString); - resultJson.put("transformationResult", getTestResultJson(transformationTestResult)); + resultJson.put("transformationResult", transformationTestResult.asJsonObject()); if (!suppressAnalysisExceptions && transformationTestResult.analysisException != null) { throw transformationTestResult.analysisException; } return transformationTestResult; } catch (Exception e) { - resultJson.put("exception", getExceptionJson(e)); + resultJson.put("exception", AndroidTestUtil.exceptionAsJsonObject(e)); throw e; } finally { writeTestSummaryToFile(context, testId, resultJson); @@ -308,7 +306,7 @@ public class TransformerAndroidTestRunner { private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson) throws IOException, JSONException { - resultJson.put("testId", testId).put("device", getDeviceJson()); + resultJson.put("testId", testId).put("device", AndroidTestUtil.getDeviceDetailsAsJsonObject()); String analysisContents = resultJson.toString(/* indentSpaces= */ 2); @@ -321,53 +319,4 @@ public class TransformerAndroidTestRunner { fileWriter.write(analysisContents); } } - - private static JSONObject getDeviceJson() throws JSONException { - return new JSONObject() - .put("manufacturer", Build.MANUFACTURER) - .put("model", Build.MODEL) - .put("sdkVersion", Build.VERSION.SDK_INT) - .put("fingerprint", Build.FINGERPRINT); - } - - private static JSONObject getTestResultJson(TransformationTestResult testResult) - throws JSONException { - TransformationResult transformationResult = testResult.transformationResult; - - JSONObject transformationResultJson = new JSONObject(); - if (transformationResult.durationMs != C.LENGTH_UNSET) { - transformationResultJson.put("durationMs", transformationResult.durationMs); - } - if (transformationResult.fileSizeBytes != C.LENGTH_UNSET) { - transformationResultJson.put("fileSizeBytes", transformationResult.fileSizeBytes); - } - if (transformationResult.averageAudioBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageAudioBitrate", transformationResult.averageAudioBitrate); - } - if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { - transformationResultJson.put("averageVideoBitrate", transformationResult.averageVideoBitrate); - } - if (testResult.elapsedTimeMs != C.TIME_UNSET) { - transformationResultJson.put("elapsedTimeMs", testResult.elapsedTimeMs); - } - if (testResult.ssim != TransformationTestResult.SSIM_UNSET) { - transformationResultJson.put("ssim", testResult.ssim); - } - if (testResult.analysisException != null) { - transformationResultJson.put( - "analysisException", getExceptionJson(testResult.analysisException)); - } - return transformationResultJson; - } - - private static JSONObject getExceptionJson(Exception exception) throws JSONException { - JSONObject exceptionJson = new JSONObject(); - exceptionJson.put("message", exception.getMessage()); - exceptionJson.put("type", exception.getClass()); - if (exception instanceof TransformationException) { - exceptionJson.put("errorCode", ((TransformationException) exception).errorCode); - } - exceptionJson.put("stackTrace", Log.getThrowableString(exception)); - return exceptionJson; - } } From bd5ca15af6218a1299b5d09950388ffdb032d722 Mon Sep 17 00:00:00 2001 From: christosts Date: Wed, 30 Mar 2022 13:54:41 +0100 Subject: [PATCH 026/116] Fallback to chunkful preparation if CODECS does not contain audio Issue: google/ExoPlayer#10065 #minor-release PiperOrigin-RevId: 438281023 --- RELEASENOTES.md | 4 ++++ .../java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index ae2a28d49b..dcd46f2192 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -35,6 +35,10 @@ views to be used with other `Player` implementations, and removes the dependency from the UI module to the ExoPlayer module. This is a breaking change. +* HLS: + * Fallback to chunkful preparation if the playlist CODECS attribute + does not contain the audio codec + ([#10065](https://github.com/google/ExoPlayer/issues/10065)). * RTSP: * Add RTP reader for MPEG4 ([#35](https://github.com/androidx/media/pull/35)) diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java index b5822578f8..101bb7a2ed 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaPeriod.java @@ -646,7 +646,8 @@ public final class HlsMediaPeriod int numberOfVideoCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_VIDEO); int numberOfAudioCodecs = Util.getCodecCountOfType(codecs, C.TRACK_TYPE_AUDIO); boolean codecsStringAllowsChunklessPreparation = - numberOfAudioCodecs <= 1 + (numberOfAudioCodecs == 1 + || (numberOfAudioCodecs == 0 && multivariantPlaylist.audios.isEmpty())) && numberOfVideoCodecs <= 1 && numberOfAudioCodecs + numberOfVideoCodecs > 0; @C.TrackType From 81ab6c730dd7f87da68bc93ac8b55056ad447ae0 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 30 Mar 2022 18:22:23 +0100 Subject: [PATCH 027/116] Fix non-inclusive language in class names. https://source.android.com/setup/contribute/respectful-code#term-examples PiperOrigin-RevId: 438335305 --- RELEASENOTES.md | 4 ++ ...Source.java => PlaceholderDataSource.java} | 12 ++--- .../datasource/cache/CacheDataSource.java | 4 +- .../video/MediaCodecVideoRenderer.java | 48 +++++++++---------- ...mySurface.java => PlaceholderSurface.java} | 41 ++++++++-------- .../video/VideoFrameReleaseHelper.java | 2 +- .../offline/DefaultDownloaderFactoryTest.java | 4 +- .../media3/exoplayer/dash/DashUtilTest.java | 10 ++-- .../dash/offline/DashDownloaderTest.java | 6 +-- .../hls/offline/HlsDownloaderTest.java | 6 +-- .../offline/SsDownloaderTest.java | 6 +-- .../media3/test/utils/CacheAsserts.java | 4 +- 12 files changed, 76 insertions(+), 71 deletions(-) rename libraries/datasource/src/main/java/androidx/media3/datasource/{DummyDataSource.java => PlaceholderDataSource.java} (77%) rename libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/{DummySurface.java => PlaceholderSurface.java} (82%) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index dcd46f2192..e87ec82783 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -15,6 +15,8 @@ * Track selection: * Flatten `TrackSelectionOverrides` class into `TrackSelectionParameters`, and promote `TrackSelectionOverride` to a top level class. +* Video: + * Rename `DummySurface` to `PlaceHolderSurface`. * Audio: * Use LG AC3 audio decoder advertising non-standard MIME type. * Extractors: @@ -51,6 +53,8 @@ ([#47](https://github.com/androidx/media/pull/47)). * Add RTP reader for WAV ([#56](https://github.com/androidx/media/pull/56)). +* Data sources: + * Rename `DummyDataSource` to `PlaceHolderDataSource`. * Remove deprecated symbols: * Remove `Player.Listener.onTracksChanged`. Use `Player.Listener.onTracksInfoChanged` instead. diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java similarity index 77% rename from libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java rename to libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java index 0c70595d73..dd29920192 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DummyDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/PlaceholderDataSource.java @@ -22,14 +22,14 @@ import java.io.IOException; /** A DataSource which provides no data. {@link #open(DataSpec)} throws {@link IOException}. */ @UnstableApi -public final class DummyDataSource implements DataSource { +public final class PlaceholderDataSource implements DataSource { - public static final DummyDataSource INSTANCE = new DummyDataSource(); + public static final PlaceholderDataSource INSTANCE = new PlaceholderDataSource(); - /** A factory that produces {@link DummyDataSource}. */ - public static final Factory FACTORY = DummyDataSource::new; + /** A factory that produces {@link PlaceholderDataSource}. */ + public static final Factory FACTORY = PlaceholderDataSource::new; - private DummyDataSource() {} + private PlaceholderDataSource() {} @Override public void addTransferListener(TransferListener transferListener) { @@ -38,7 +38,7 @@ public final class DummyDataSource implements DataSource { @Override public long open(DataSpec dataSpec) throws IOException { - throw new IOException("DummyDataSource cannot be opened"); + throw new IOException("PlaceholderDataSource cannot be opened"); } @Override diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java index 4eae94a1ee..66bb73e4ad 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/cache/CacheDataSource.java @@ -36,8 +36,8 @@ import androidx.media3.datasource.DataSink; import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceException; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; import androidx.media3.datasource.FileDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.PriorityDataSource; import androidx.media3.datasource.TeeDataSource; import androidx.media3.datasource.TransferListener; @@ -541,7 +541,7 @@ public final class CacheDataSource implements DataSource { ? new TeeDataSource(upstreamDataSource, cacheWriteDataSink) : null; } else { - this.upstreamDataSource = DummyDataSource.INSTANCE; + this.upstreamDataSource = PlaceholderDataSource.INSTANCE; this.cacheWriteDataSource = null; } this.eventListener = eventListener; diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java index d1114f4ca3..cf38dc6a13 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/MediaCodecVideoRenderer.java @@ -129,7 +129,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { private boolean codecHandlesHdr10PlusOutOfBandMetadata; @Nullable private Surface surface; - @Nullable private DummySurface dummySurface; + @Nullable private PlaceholderSurface placeholderSurface; private boolean haveReportedFirstFrameRenderedForCurrentSurface; private @C.VideoScalingMode int scalingMode; private boolean renderedFirstFrameAfterReset; @@ -515,7 +515,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { public boolean isReady() { if (super.isReady() && (renderedFirstFrameAfterReset - || (dummySurface != null && surface == dummySurface) + || (placeholderSurface != null && surface == placeholderSurface) || getCodec() == null || tunneling)) { // Ready. If we were joining then we've now joined, so clear the joining deadline. @@ -567,14 +567,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { } } - @TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16. + @TargetApi(17) // Needed for placeholderSurface usage, as it is always null on API level 16. @Override protected void onReset() { try { super.onReset(); } finally { - if (dummySurface != null) { - releaseDummySurface(); + if (placeholderSurface != null) { + releasePlaceholderSurface(); } } } @@ -624,14 +624,14 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { @Nullable Surface surface = output instanceof Surface ? (Surface) output : null; if (surface == null) { - // Use a dummy surface if possible. - if (dummySurface != null) { - surface = dummySurface; + // Use a placeholder surface if possible. + if (placeholderSurface != null) { + surface = placeholderSurface; } else { MediaCodecInfo codecInfo = getCodecInfo(); if (codecInfo != null && shouldUseDummySurface(codecInfo)) { - dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); - surface = dummySurface; + placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure); + surface = placeholderSurface; } } } @@ -652,7 +652,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { maybeInitCodecOrBypass(); } } - if (surface != null && surface != dummySurface) { + if (surface != null && surface != placeholderSurface) { // If we know the video size, report it again immediately. maybeRenotifyVideoSizeChanged(); // We haven't rendered to the new surface yet. @@ -665,7 +665,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { clearReportedVideoSize(); clearRenderedFirstFrame(); } - } else if (surface != null && surface != dummySurface) { + } else if (surface != null && surface != placeholderSurface) { // The surface is set and unchanged. If we know the video size and/or have already rendered to // the surface, report these again immediately. maybeRenotifyVideoSizeChanged(); @@ -684,16 +684,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return tunneling && Util.SDK_INT < 23; } - @TargetApi(17) // Needed for dummySurface usage. dummySurface is always null on API level 16. + @TargetApi(17) // Needed for placeHolderSurface usage, as it is always null on API level 16. @Override protected MediaCodecAdapter.Configuration getMediaCodecConfiguration( MediaCodecInfo codecInfo, Format format, @Nullable MediaCrypto crypto, float codecOperatingRate) { - if (dummySurface != null && dummySurface.secure != codecInfo.secure) { + if (placeholderSurface != null && placeholderSurface.secure != codecInfo.secure) { // We can't re-use the current DummySurface instance with the new decoder. - releaseDummySurface(); + releasePlaceholderSurface(); } String codecMimeType = codecInfo.codecMimeType; codecMaxValues = getCodecMaxValues(codecInfo, format, getStreamFormats()); @@ -709,10 +709,10 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { if (!shouldUseDummySurface(codecInfo)) { throw new IllegalStateException(); } - if (dummySurface == null) { - dummySurface = DummySurface.newInstanceV17(context, codecInfo.secure); + if (placeholderSurface == null) { + placeholderSurface = PlaceholderSurface.newInstanceV17(context, codecInfo.secure); } - surface = dummySurface; + surface = placeholderSurface; } return MediaCodecAdapter.Configuration.createForVideoDecoding( codecInfo, mediaFormat, format, surface, crypto); @@ -949,7 +949,7 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { earlyUs -= elapsedRealtimeNowUs - elapsedRealtimeUs; } - if (surface == dummySurface) { + if (surface == placeholderSurface) { // Skip frames in sync with playback, so we'll be at the right frame if the mode changes. if (isBufferLate(earlyUs)) { skipOutputBuffer(codec, bufferIndex, presentationTimeUs); @@ -1259,16 +1259,16 @@ public class MediaCodecVideoRenderer extends MediaCodecRenderer { return Util.SDK_INT >= 23 && !tunneling && !codecNeedsSetOutputSurfaceWorkaround(codecInfo.name) - && (!codecInfo.secure || DummySurface.isSecureSupported(context)); + && (!codecInfo.secure || PlaceholderSurface.isSecureSupported(context)); } @RequiresApi(17) - private void releaseDummySurface() { - if (surface == dummySurface) { + private void releasePlaceholderSurface() { + if (surface == placeholderSurface) { surface = null; } - dummySurface.release(); - dummySurface = null; + placeholderSurface.release(); + placeholderSurface = null; } private void setJoiningDeadlineMs() { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java similarity index 82% rename from libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java rename to libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java index a7819ddd25..e9d1955435 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/DummySurface.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/PlaceholderSurface.java @@ -36,12 +36,12 @@ import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; -/** A dummy {@link Surface}. */ +/** A placeholder {@link Surface}. */ @RequiresApi(17) @UnstableApi -public final class DummySurface extends Surface { +public final class PlaceholderSurface extends Surface { - private static final String TAG = "DummySurface"; + private static final String TAG = "PlaceholderSurface"; /** Whether the surface is secure. */ public final boolean secure; @@ -49,14 +49,14 @@ public final class DummySurface extends Surface { private static @SecureMode int secureMode; private static boolean secureModeInitialized; - private final DummySurfaceThread thread; + private final PlaceholderSurfaceThread thread; private boolean threadReleased; /** - * Returns whether the device supports secure dummy surfaces. + * Returns whether the device supports secure placeholder surfaces. * * @param context Any {@link Context}. - * @return Whether the device supports secure dummy surfaces. + * @return Whether the device supports secure placeholder surfaces. */ public static synchronized boolean isSecureSupported(Context context) { if (!secureModeInitialized) { @@ -67,8 +67,8 @@ public final class DummySurface extends Surface { } /** - * Returns a newly created dummy surface. The surface must be released by calling {@link #release} - * when it's no longer required. + * Returns a newly created placeholder surface. The surface must be released by calling {@link + * #release} when it's no longer required. * *

Must only be called if {@link Util#SDK_INT} is 17 or higher. * @@ -78,13 +78,14 @@ public final class DummySurface extends Surface { * @throws IllegalStateException If a secure surface is requested on a device for which {@link * #isSecureSupported(Context)} returns {@code false}. */ - public static DummySurface newInstanceV17(Context context, boolean secure) { + public static PlaceholderSurface newInstanceV17(Context context, boolean secure) { Assertions.checkState(!secure || isSecureSupported(context)); - DummySurfaceThread thread = new DummySurfaceThread(); + PlaceholderSurfaceThread thread = new PlaceholderSurfaceThread(); return thread.init(secure ? secureMode : SECURE_MODE_NONE); } - private DummySurface(DummySurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { + private PlaceholderSurface( + PlaceholderSurfaceThread thread, SurfaceTexture surfaceTexture, boolean secure) { super(surfaceTexture); this.thread = thread; this.secure = secure; @@ -121,7 +122,7 @@ public final class DummySurface extends Surface { } } - private static class DummySurfaceThread extends HandlerThread implements Handler.Callback { + private static class PlaceholderSurfaceThread extends HandlerThread implements Handler.Callback { private static final int MSG_INIT = 1; private static final int MSG_RELEASE = 2; @@ -130,13 +131,13 @@ public final class DummySurface extends Surface { private @MonotonicNonNull Handler handler; @Nullable private Error initError; @Nullable private RuntimeException initException; - @Nullable private DummySurface surface; + @Nullable private PlaceholderSurface surface; - public DummySurfaceThread() { - super("ExoPlayer:DummySurface"); + public PlaceholderSurfaceThread() { + super("ExoPlayer:PlaceholderSurface"); } - public DummySurface init(@SecureMode int secureMode) { + public PlaceholderSurface init(@SecureMode int secureMode) { start(); handler = new Handler(getLooper(), /* callback= */ this); eglSurfaceTexture = new EGLSurfaceTexture(handler); @@ -176,10 +177,10 @@ public final class DummySurface extends Surface { try { initInternal(/* secureMode= */ msg.arg1); } catch (RuntimeException e) { - Log.e(TAG, "Failed to initialize dummy surface", e); + Log.e(TAG, "Failed to initialize placeholder surface", e); initException = e; } catch (Error e) { - Log.e(TAG, "Failed to initialize dummy surface", e); + Log.e(TAG, "Failed to initialize placeholder surface", e); initError = e; } finally { synchronized (this) { @@ -191,7 +192,7 @@ public final class DummySurface extends Surface { try { releaseInternal(); } catch (Throwable e) { - Log.e(TAG, "Failed to release dummy surface", e); + Log.e(TAG, "Failed to release placeholder surface", e); } finally { quit(); } @@ -205,7 +206,7 @@ public final class DummySurface extends Surface { Assertions.checkNotNull(eglSurfaceTexture); eglSurfaceTexture.init(secureMode); this.surface = - new DummySurface( + new PlaceholderSurface( this, eglSurfaceTexture.getSurfaceTexture(), secureMode != SECURE_MODE_NONE); } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java index 00aac0e00e..bfac6433f6 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/video/VideoFrameReleaseHelper.java @@ -168,7 +168,7 @@ public final class VideoFrameReleaseHelper { * @param surface The new {@link Surface}, or {@code null} if the renderer does not have one. */ public void onSurfaceChanged(@Nullable Surface surface) { - if (surface instanceof DummySurface) { + if (surface instanceof PlaceholderSurface) { // We don't care about dummy surfaces for release timing, since they're not visible. surface = null; } diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java index 33c2c5bba1..2dcc9fdae3 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/offline/DefaultDownloaderFactoryTest.java @@ -18,7 +18,7 @@ package androidx.media3.exoplayer.offline; import static com.google.common.truth.Truth.assertThat; import android.net.Uri; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -35,7 +35,7 @@ public final class DefaultDownloaderFactoryTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java index 4afe48ee38..c1e6f1524e 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/DashUtilTest.java @@ -22,7 +22,7 @@ import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; import androidx.media3.common.Format; import androidx.media3.common.MimeTypes; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.exoplayer.dash.manifest.AdaptationSet; import androidx.media3.exoplayer.dash.manifest.BaseUrl; import androidx.media3.exoplayer.dash.manifest.Period; @@ -43,28 +43,28 @@ public final class DashUtilTest { @Test public void loadDrmInitDataFromManifest() throws Exception { Period period = newPeriod(newAdaptationSet(newRepresentation(newDrmInitData()))); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format.drmInitData).isEqualTo(newDrmInitData()); } @Test public void loadDrmInitDataMissing() throws Exception { Period period = newPeriod(newAdaptationSet(newRepresentation(null /* no init data */))); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format.drmInitData).isNull(); } @Test public void loadDrmInitDataNoRepresentations() throws Exception { Period period = newPeriod(newAdaptationSet(/* no representation */ )); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format).isNull(); } @Test public void loadDrmInitDataNoAdaptationSets() throws Exception { Period period = newPeriod(/* no adaptation set */ ); - Format format = DashUtil.loadFormatWithDrmInitData(DummyDataSource.INSTANCE, period); + Format format = DashUtil.loadFormatWithDrmInitData(PlaceholderDataSource.INSTANCE, period); assertThat(format).isNull(); } diff --git a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java index 46b90628c0..b42a77ee42 100644 --- a/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java +++ b/libraries/exoplayer_dash/src/test/java/androidx/media3/exoplayer/dash/offline/DashDownloaderTest.java @@ -31,7 +31,7 @@ import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -86,7 +86,7 @@ public class DashDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -96,7 +96,7 @@ public class DashDownloaderTest { .setMimeType(MimeTypes.APPLICATION_MPD) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(DashDownloader.class); } diff --git a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java index 775323ce32..4e239e3502 100644 --- a/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java +++ b/libraries/exoplayer_hls/src/test/java/androidx/media3/exoplayer/hls/offline/HlsDownloaderTest.java @@ -39,7 +39,7 @@ import androidx.media3.common.MediaItem; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; import androidx.media3.common.util.Util; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.datasource.cache.NoOpCacheEvictor; @@ -104,7 +104,7 @@ public class HlsDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -114,7 +114,7 @@ public class HlsDownloaderTest { .setMimeType(MimeTypes.APPLICATION_M3U8) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(HlsDownloader.class); } diff --git a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java index fcaa18605d..dc73b98a2f 100644 --- a/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java +++ b/libraries/exoplayer_smoothstreaming/src/test/java/androidx/media3/exoplayer/smoothstreaming/offline/SsDownloaderTest.java @@ -20,7 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import android.net.Uri; import androidx.media3.common.MimeTypes; import androidx.media3.common.StreamKey; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.exoplayer.offline.DefaultDownloaderFactory; @@ -42,7 +42,7 @@ public final class SsDownloaderTest { CacheDataSource.Factory cacheDataSourceFactory = new CacheDataSource.Factory() .setCache(Mockito.mock(Cache.class)) - .setUpstreamDataSourceFactory(DummyDataSource.FACTORY); + .setUpstreamDataSourceFactory(PlaceholderDataSource.FACTORY); DownloaderFactory factory = new DefaultDownloaderFactory(cacheDataSourceFactory, /* executor= */ Runnable::run); @@ -52,7 +52,7 @@ public final class SsDownloaderTest { .setMimeType(MimeTypes.APPLICATION_SS) .setStreamKeys( Collections.singletonList( - new StreamKey(/* groupIndex= */ 0, /* trackIndex= */ 0))) + new StreamKey(/* groupIndex= */ 0, /* streamIndex= */ 0))) .build()); assertThat(downloader).isInstanceOf(SsDownloader.class); } diff --git a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java index 864abd634f..b7bf6bcc47 100644 --- a/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java +++ b/libraries/test_utils/src/main/java/androidx/media3/test/utils/CacheAsserts.java @@ -26,7 +26,7 @@ import androidx.media3.datasource.DataSource; import androidx.media3.datasource.DataSourceInputStream; import androidx.media3.datasource.DataSourceUtil; import androidx.media3.datasource.DataSpec; -import androidx.media3.datasource.DummyDataSource; +import androidx.media3.datasource.PlaceholderDataSource; import androidx.media3.datasource.cache.Cache; import androidx.media3.datasource.cache.CacheDataSource; import androidx.media3.test.utils.FakeDataSet.FakeData; @@ -129,7 +129,7 @@ public final class CacheAsserts { */ public static void assertDataCached(Cache cache, DataSpec dataSpec, byte[] expected) throws IOException { - DataSource dataSource = new CacheDataSource(cache, DummyDataSource.INSTANCE, 0); + DataSource dataSource = new CacheDataSource(cache, PlaceholderDataSource.INSTANCE, 0); byte[] bytes; try { dataSource.open(dataSpec); From 22ca225fa3745286269ce27295633ffd2c90b971 Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 31 Mar 2022 09:09:44 +0100 Subject: [PATCH 028/116] Stabilise MIME types that can be passed to MediaItem.Builder This is basically 'container' and 'subtitle' MIME types I previously avoided stabilising any 'custom' MIME types (those containing '/x-') but it certainly seems reasonable to expect developers to use APPLICATION_M3U8 and so then it also makes sense to stabilise other 'similar' custom MIME types too. PiperOrigin-RevId: 438501642 --- .../src/main/java/androidx/media3/common/MimeTypes.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java index 2c6973d1e1..49de1ef802 100644 --- a/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java +++ b/libraries/common/src/main/java/androidx/media3/common/MimeTypes.java @@ -108,11 +108,10 @@ public final class MimeTypes { public static final String APPLICATION_MP4 = BASE_TYPE_APPLICATION + "/mp4"; public static final String APPLICATION_WEBM = BASE_TYPE_APPLICATION + "/webm"; - @UnstableApi public static final String APPLICATION_MATROSKA = BASE_TYPE_APPLICATION + "/x-matroska"; public static final String APPLICATION_MPD = BASE_TYPE_APPLICATION + "/dash+xml"; - @UnstableApi public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; + public static final String APPLICATION_M3U8 = BASE_TYPE_APPLICATION + "/x-mpegURL"; public static final String APPLICATION_SS = BASE_TYPE_APPLICATION + "/vnd.ms-sstr+xml"; public static final String APPLICATION_ID3 = BASE_TYPE_APPLICATION + "/id3"; public static final String APPLICATION_CEA608 = BASE_TYPE_APPLICATION + "/cea-608"; @@ -135,7 +134,7 @@ public final class MimeTypes { @UnstableApi public static final String APPLICATION_EXIF = BASE_TYPE_APPLICATION + "/x-exif"; @UnstableApi public static final String APPLICATION_ICY = BASE_TYPE_APPLICATION + "/x-icy"; public static final String APPLICATION_AIT = BASE_TYPE_APPLICATION + "/vnd.dvb.ait"; - @UnstableApi public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp"; + public static final String APPLICATION_RTSP = BASE_TYPE_APPLICATION + "/x-rtsp"; // image/ MIME types From aadbf3d59bc6ee3428d656ce252e94d0e5cd16be Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 31 Mar 2022 12:13:42 +0100 Subject: [PATCH 029/116] glClear in FrameProcessorChain so the GlFrameProcessors don't have to. Since the output textures and surfaces are managed by the FrameProcessorChain, clearing them there makes sense. This is also less error-prone as it might not be obvious to someone implementing a GlFrameProcessor that they need to glClear. (Clearing twice won't cause any problems.) PiperOrigin-RevId: 438532247 --- .../media3/transformer/AdvancedFrameProcessor.java | 3 --- .../transformer/ExternalCopyFrameProcessor.java | 2 -- .../media3/transformer/FrameProcessorChain.java | 14 +++++++++++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index fb9b22519d..8857d25ddf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -128,9 +128,6 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { checkStateNotNull(glProgram); glProgram.use(); glProgram.bindAttributesAndUniforms(); - GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); - GlUtil.checkGlError(); // The four-vertex triangle strip forms a quad. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); GlUtil.checkGlError(); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 502f1fb167..47eef213d6 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -105,8 +105,6 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; checkStateNotNull(glProgram); glProgram.use(); glProgram.bindAttributesAndUniforms(); - GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); // The four-vertex triangle strip forms a quad. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 9645a1d52a..c9f59d1450 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -92,7 +92,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private final float[] textureTransformMatrix; private final ExternalCopyFrameProcessor externalCopyFrameProcessor; - private final List frameProcessors; + private final ImmutableList frameProcessors; /** * Identifiers of a framebuffer object associated with the intermediate textures that receive * output from the previous {@link GlFrameProcessor}, and provide input for the following {@link @@ -413,6 +413,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix); long presentationTimeNs = inputSurfaceTexture.getTimestamp(); long presentationTimeUs = presentationTimeNs / 1000; + clearOutputFrame(); externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs); for (int i = 0; i < frameProcessors.size() - 1; i++) { @@ -424,10 +425,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; framebuffers[i + 1], outputSize.getWidth(), outputSize.getHeight()); + clearOutputFrame(); frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); } if (!frameProcessors.isEmpty()) { GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); + clearOutputFrame(); getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); } @@ -437,8 +440,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; if (debugPreviewEglSurface != null) { GlUtil.focusEglSurface( eglDisplay, eglContext, debugPreviewEglSurface, debugPreviewWidth, debugPreviewHeight); - GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); - GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + clearOutputFrame(); // The four-vertex triangle strip forms a quad. GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, /* first= */ 0, /* count= */ 4); EGL14.eglSwapBuffers(eglDisplay, debugPreviewEglSurface); @@ -447,6 +449,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; checkState(pendingFrameCount.getAndDecrement() > 0); } + private static void clearOutputFrame() { + GLES20.glClearColor(/* red= */ 0, /* green= */ 0, /* blue= */ 0, /* alpha= */ 0); + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GlUtil.checkGlError(); + } + /** * Configures the input and output {@linkplain Size sizes} of a list of {@link GlFrameProcessor * GlFrameProcessors}. From bd257d24edb9cdb4c89c73eb3c8ddbc78831c5ab Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 31 Mar 2022 12:30:39 +0100 Subject: [PATCH 030/116] Suppress spurious unchecked cast warning in LibraryResult PiperOrigin-RevId: 438534391 --- .../src/main/java/androidx/media3/session/LibraryResult.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java index f602e35379..4e469926c3 100644 --- a/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java +++ b/libraries/session/src/main/java/androidx/media3/session/LibraryResult.java @@ -279,6 +279,8 @@ public final class LibraryResult implements Bundleable { private static final int FIELD_VALUE = 3; private static final int FIELD_VALUE_TYPE = 4; + // Casting V to ImmutableList is safe if valueType == VALUE_TYPE_ITEM_LIST. + @SuppressWarnings("unchecked") @UnstableApi @Override public Bundle toBundle() { From 839f342e55bd8080db6d4cb1ca6d82cab3ad7e3c Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 31 Mar 2022 13:20:56 +0100 Subject: [PATCH 031/116] Use placeholder surface to configure OpenGL and frame processors. The placeholder surface is either EGL_NO_SURFACE or a 1x1 pbuffer depending on whether the device supports EGL_KHR_surfaceless_context. PiperOrigin-RevId: 438541846 --- .../androidx/media3/common/util/GlUtil.java | 65 ++++++++++++++++- .../transformer/FrameProcessorChain.java | 72 +++++++++++-------- 2 files changed, 108 insertions(+), 29 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java index f29b8f0503..fe4532976c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/GlUtil.java @@ -147,7 +147,11 @@ public final class GlUtil { } /** - * Returns whether creating a GL context with {@value #EXTENSION_SURFACELESS_CONTEXT} is possible. + * Returns whether the {@value #EXTENSION_SURFACELESS_CONTEXT} extension is supported. + * + *

This extension allows passing {@link EGL14#EGL_NO_SURFACE} for both the write and read + * surfaces in a call to {@link EGL14#eglMakeCurrent(EGLDisplay, EGLSurface, EGLSurface, + * EGLContext)}. */ public static boolean isSurfacelessContextExtensionSupported() { if (Util.SDK_INT < 17) { @@ -207,6 +211,52 @@ public final class GlUtil { EGL_WINDOW_SURFACE_ATTRIBUTES_BT2020_PQ); } + /** + * Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer. + * + * @param eglContext The {@link EGLContext} to make current. + * @param eglDisplay The {@link EGLDisplay} to attach the surface to. + */ + @RequiresApi(17) + public static void focusPlaceholderEglSurface(EGLContext eglContext, EGLDisplay eglDisplay) { + int[] pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, /* width= */ 1, + EGL14.EGL_HEIGHT, /* height= */ 1, + EGL14.EGL_NONE + }; + EGLSurface eglSurface = + Api17.createEglPbufferSurface( + eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_8888, pbufferAttributes); + focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); + } + + /** + * Creates and focuses a new {@link EGLSurface} wrapping a 1x1 pixel buffer, for HDR rendering + * with Rec. 2020 color primaries and using the PQ transfer function. + * + * @param eglContext The {@link EGLContext} to make current. + * @param eglDisplay The {@link EGLDisplay} to attach the surface to. + */ + @RequiresApi(17) + public static void focusPlaceholderEglSurfaceBt2020Pq( + EGLContext eglContext, EGLDisplay eglDisplay) { + int[] pbufferAttributes = + new int[] { + EGL14.EGL_WIDTH, + /* width= */ 1, + EGL14.EGL_HEIGHT, + /* height= */ 1, + EGL_GL_COLORSPACE_KHR, + EGL_GL_COLORSPACE_BT2020_PQ_EXT, + EGL14.EGL_NONE + }; + EGLSurface eglSurface = + Api17.createEglPbufferSurface( + eglDisplay, EGL_CONFIG_ATTRIBUTES_RGBA_1010102, pbufferAttributes); + focusEglSurface(eglDisplay, eglContext, eglSurface, /* width= */ 1, /* height= */ 1); + } + /** * If there is an OpenGl error, logs the error and if {@link #glAssertionsEnabled} is true throws * a {@link GlException}. @@ -486,6 +536,19 @@ public final class GlUtil { return eglSurface; } + @DoNotInline + public static EGLSurface createEglPbufferSurface( + EGLDisplay eglDisplay, int[] configAttributes, int[] pbufferAttributes) { + EGLSurface eglSurface = + EGL14.eglCreatePbufferSurface( + eglDisplay, + getEglConfig(eglDisplay, configAttributes), + pbufferAttributes, + /* offset= */ 0); + checkEglException("Error creating surface"); + return eglSurface; + } + @DoNotInline public static void focusRenderTarget( EGLDisplay eglDisplay, diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index c9f59d1450..3e42f67a32 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -45,7 +45,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.atomic.AtomicInteger; -import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.checkerframework.checker.nullness.qual.RequiresNonNull; @@ -68,8 +67,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; private final boolean enableExperimentalHdrEditing; - private final EGLDisplay eglDisplay; - private final EGLContext eglContext; + private @MonotonicNonNull EGLDisplay eglDisplay; + private @MonotonicNonNull EGLContext eglContext; /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ private final ExecutorService singleThreadExecutorService; /** Futures corresponding to the executor service's pending tasks. */ @@ -152,16 +151,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; this.frameProcessors = ImmutableList.copyOf(frameProcessors); - try { - eglDisplay = GlUtil.createEglDisplay(); - eglContext = - enableExperimentalHdrEditing - ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) - : GlUtil.createEglContext(eglDisplay); - } catch (GlUtil.GlException e) { - throw TransformationException.createForFrameProcessorChain( - e, TransformationException.ERROR_CODE_GL_INIT_FAILED); - } singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); futures = new ConcurrentLinkedQueue<>(); pendingFrameCount = new AtomicInteger(); @@ -218,9 +207,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; try { // Wait for task to finish to be able to use inputExternalTexId to create the SurfaceTexture. singleThreadExecutorService - .submit( - () -> - createOpenGlObjectsAndInitializeFrameProcessors(outputSurface, debugSurfaceView)) + .submit(this::createOpenGlObjectsAndInitializeFrameProcessors) .get(); } catch (ExecutionException e) { throw TransformationException.createForFrameProcessorChain( @@ -248,6 +235,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } }); inputSurface = new Surface(inputSurfaceTexture); + + futures.add( + singleThreadExecutorService.submit( + () -> createOpenGlSurfaces(outputSurface, debugSurfaceView))); } /** @@ -344,16 +335,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Creates the OpenGL textures, framebuffers, surfaces, and initializes the {@link - * GlFrameProcessor GlFrameProcessors}. + * Creates the OpenGL surfaces. * - *

This method must by executed on the same thread as {@link #processFrame()}, i.e., executed - * by the {@link #singleThreadExecutorService}. + *

This method should only be called after {@link + * #createOpenGlObjectsAndInitializeFrameProcessors()} and must be called on the background + * thread. */ - @EnsuresNonNull("eglSurface") - private Void createOpenGlObjectsAndInitializeFrameProcessors( - Surface outputSurface, @Nullable SurfaceView debugSurfaceView) throws IOException { + private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + checkStateNotNull(eglDisplay); if (enableExperimentalHdrEditing) { // TODO(b/209404935): Don't assume BT.2020 PQ input/output. @@ -369,7 +359,32 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; GlUtil.getEglSurface(eglDisplay, checkNotNull(debugSurfaceView.getHolder())); } } - GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); + } + + /** + * Creates the OpenGL textures and framebuffers, and initializes the {@link GlFrameProcessor + * GlFrameProcessors}. + * + *

This method should only be called on the background thread. + */ + private Void createOpenGlObjectsAndInitializeFrameProcessors() throws IOException { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + + eglDisplay = GlUtil.createEglDisplay(); + eglContext = + enableExperimentalHdrEditing + ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) + : GlUtil.createEglContext(eglDisplay); + + if (GlUtil.isSurfacelessContextExtensionSupported()) { + GlUtil.focusEglSurface( + eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); + } else if (enableExperimentalHdrEditing) { + // TODO(b/209404935): Don't assume BT.2020 PQ input/output. + GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay); + } else { + GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); + } inputExternalTexId = GlUtil.createExternalTexture(); Size inputSize = inputSizes.get(0); @@ -389,13 +404,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Processes an input frame. * - *

This method must by executed on the same thread as {@link - * #createOpenGlObjectsAndInitializeFrameProcessors(Surface,SurfaceView)}, i.e., executed by the - * {@link #singleThreadExecutorService}. + *

This method should only be called on the background thread. */ - @RequiresNonNull({"inputSurfaceTexture", "eglSurface"}) + @RequiresNonNull("inputSurfaceTexture") private void processFrame() { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + checkStateNotNull(eglSurface); + checkStateNotNull(eglContext); + checkStateNotNull(eglDisplay); if (frameProcessors.isEmpty()) { GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); From d8e5e2de7a2daeb7b93c5ab02eacd3778d7eb19a Mon Sep 17 00:00:00 2001 From: samrobinson Date: Thu, 31 Mar 2022 13:23:44 +0100 Subject: [PATCH 032/116] Re-use test runner in each loop of RepeatedTranscodeTransformationTest PiperOrigin-RevId: 438542239 --- .../RepeatedTranscodeTransformationTest.java | 74 ++++++++++--------- 1 file changed, 40 insertions(+), 34 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java index 5b77a783be..a02be1f3e9 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/RepeatedTranscodeTransformationTest.java @@ -40,13 +40,17 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscode_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = - new Transformer.Builder(context) - .setTransformationRequest( - new TransformationRequest.Builder() - .setRotationDegrees(45) - // Video MIME type is H264. - .setAudioMimeType(MimeTypes.AUDIO_AAC) + + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setTransformationRequest( + new TransformationRequest.Builder() + .setRotationDegrees(45) + // Video MIME type is H264. + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .build()) .build()) .build(); @@ -54,11 +58,9 @@ public final class RepeatedTranscodeTransformationTest { for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. TransformationTestResult testResult = - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run( - /* testId= */ "repeatedTranscode_givesConsistentLengthOutput_" + i, - AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); + transformerRunner.run( + /* testId= */ "repeatedTranscode_givesConsistentLengthOutput_" + i, + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } @@ -71,13 +73,16 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscodeNoAudio_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = - new Transformer.Builder(context) - .setRemoveAudio(true) - .setTransformationRequest( - new TransformationRequest.Builder() - // Video MIME type is H264. - .setRotationDegrees(45) + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setRemoveAudio(true) + .setTransformationRequest( + new TransformationRequest.Builder() + // Video MIME type is H264. + .setRotationDegrees(45) + .build()) .build()) .build(); @@ -85,11 +90,9 @@ public final class RepeatedTranscodeTransformationTest { for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. TransformationTestResult testResult = - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run( - /* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput_" + i, - AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); + transformerRunner.run( + /* testId= */ "repeatedTranscodeNoAudio_givesConsistentLengthOutput_" + i, + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } @@ -102,22 +105,25 @@ public final class RepeatedTranscodeTransformationTest { @Test public void repeatedTranscodeNoVideo_givesConsistentLengthOutput() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - Transformer transformer = - new Transformer.Builder(context) - .setRemoveVideo(true) - .setTransformationRequest( - new TransformationRequest.Builder().setAudioMimeType(MimeTypes.AUDIO_AAC).build()) + TransformerAndroidTestRunner transformerRunner = + new TransformerAndroidTestRunner.Builder( + context, + new Transformer.Builder(context) + .setRemoveVideo(true) + .setTransformationRequest( + new TransformationRequest.Builder() + .setAudioMimeType(MimeTypes.AUDIO_AAC) + .build()) + .build()) .build(); Set differentOutputSizesBytes = new HashSet<>(); for (int i = 0; i < TRANSCODE_COUNT; i++) { // Use a long video in case an error occurs a while after the start of the video. TransformationTestResult testResult = - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run( - /* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput_" + i, - AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); + transformerRunner.run( + /* testId= */ "repeatedTranscodeNoVideo_givesConsistentLengthOutput_" + i, + AndroidTestUtil.MP4_REMOTE_H264_MP3_URI_STRING); differentOutputSizesBytes.add(checkNotNull(testResult.transformationResult.fileSizeBytes)); } From 70cffd8cca3885d449fefe5806f63bec3e3ef417 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Thu, 31 Mar 2022 13:30:45 +0100 Subject: [PATCH 033/116] Split configureOutputSize into setInputSize and getOutputSize. This makes it easier (smaller CL diff) to merge output size configuration and initialize() in a follow-up. PiperOrigin-RevId: 438543247 --- .../transformer/AdvancedFrameProcessor.java | 10 ++- .../ExternalCopyFrameProcessor.java | 10 ++- .../transformer/FrameProcessorChain.java | 70 +++++++------------ .../media3/transformer/GlFrameProcessor.java | 17 ++++- .../PresentationFrameProcessor.java | 16 +++-- .../transformer/ScaleToFitFrameProcessor.java | 16 +++-- .../AdvancedFrameProcessorTest.java | 12 ++-- .../transformer/FrameProcessorChainTest.java | 5 +- .../PresentationFrameProcessorTest.java | 22 +++--- .../ScaleToFitFrameProcessorTest.java | 30 ++++---- 10 files changed, 119 insertions(+), 89 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index 8857d25ddf..6996c0111c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -89,6 +89,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { private final Context context; private final Matrix transformationMatrix; + private @MonotonicNonNull Size size; private @MonotonicNonNull GlProgram glProgram; /** @@ -105,8 +106,13 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { } @Override - public Size configureOutputSize(int inputWidth, int inputHeight) { - return new Size(inputWidth, inputHeight); + public void setInputSize(int inputWidth, int inputHeight) { + size = new Size(inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(size); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 47eef213d6..8fa78ec3fc 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -51,6 +51,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; private final Context context; private final boolean enableExperimentalHdrEditing; + private @MonotonicNonNull Size size; private @MonotonicNonNull GlProgram glProgram; public ExternalCopyFrameProcessor(Context context, boolean enableExperimentalHdrEditing) { @@ -59,8 +60,13 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public Size configureOutputSize(int inputWidth, int inputHeight) { - return new Size(inputWidth, inputHeight); + public void setInputSize(int inputWidth, int inputHeight) { + size = new Size(inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(size); } @Override diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 3e42f67a32..0dfdcb895e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -28,7 +28,6 @@ import android.opengl.EGLDisplay; import android.opengl.EGLExt; import android.opengl.EGLSurface; import android.opengl.GLES20; -import android.util.Pair; import android.util.Size; import android.view.Surface; import android.view.SurfaceView; @@ -79,6 +78,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private volatile boolean releaseRequested; private boolean inputStreamEnded; + private final Size inputSize; /** Wraps the {@link #inputSurfaceTexture}. */ private @MonotonicNonNull Surface inputSurface; /** Associated with an OpenGL external texture. */ @@ -100,11 +100,8 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; *

The {@link ExternalCopyFrameProcessor} writes to the first framebuffer. */ private final int[] framebuffers; - /** The input {@link Size} of each of the {@code frameProcessors}. */ - private final ImmutableList inputSizes; - private int outputWidth; - private int outputHeight; + private Size outputSize; /** * Wraps the output {@link Surface} that is populated with the output of the final {@link * GlFrameProcessor} for each frame. @@ -154,30 +151,27 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); futures = new ConcurrentLinkedQueue<>(); pendingFrameCount = new AtomicInteger(); + inputSize = new Size(inputWidth, inputHeight); textureTransformMatrix = new float[16]; externalCopyFrameProcessor = new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); framebuffers = new int[frameProcessors.size()]; - Pair, Size> sizes = - configureFrameProcessorSizes(inputWidth, inputHeight, frameProcessors); - inputSizes = sizes.first; - outputWidth = sizes.second.getWidth(); - outputHeight = sizes.second.getHeight(); + configureFrameProcessorSizes(inputSize, frameProcessors); + outputSize = frameProcessors.isEmpty() ? inputSize : getLast(frameProcessors).getOutputSize(); debugPreviewWidth = C.LENGTH_UNSET; debugPreviewHeight = C.LENGTH_UNSET; } /** Returns the output {@link Size}. */ public Size getOutputSize() { - return new Size(outputWidth, outputHeight); + return outputSize; } /** * Configures the {@code FrameProcessorChain} to process frames to the specified output targets. * *

This method may only be called once and may override the {@linkplain - * GlFrameProcessor#configureOutputSize(int, int) output size} of the final {@link - * GlFrameProcessor}. + * GlFrameProcessor#setInputSize(int, int) output size} of the final {@link GlFrameProcessor}. * * @param outputSurface The output {@link Surface}. * @param outputWidth The output width, in pixels. @@ -196,8 +190,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; checkState(inputSurface == null, "The FrameProcessorChain has already been configured."); // TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final // GlFrameProcessor to be re-configured or append another GlFrameProcessor. - this.outputWidth = outputWidth; - this.outputHeight = outputHeight; + outputSize = new Size(outputWidth, outputHeight); if (debugSurfaceView != null) { debugPreviewWidth = debugSurfaceView.getWidth(); @@ -387,15 +380,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } inputExternalTexId = GlUtil.createExternalTexture(); - Size inputSize = inputSizes.get(0); - externalCopyFrameProcessor.configureOutputSize(inputSize.getWidth(), inputSize.getHeight()); + externalCopyFrameProcessor.setInputSize(inputSize.getWidth(), inputSize.getHeight()); externalCopyFrameProcessor.initialize(inputExternalTexId); + Size intermediateSize = inputSize; for (int i = 0; i < frameProcessors.size(); i++) { - inputSize = inputSizes.get(i); - int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight()); + int inputTexId = + GlUtil.createTexture(intermediateSize.getWidth(), intermediateSize.getHeight()); framebuffers[i] = GlUtil.createFboForTexture(inputTexId); frameProcessors.get(i).initialize(inputTexId); + intermediateSize = frameProcessors.get(i).getOutputSize(); } // Return something because only Callables not Runnables can throw checked exceptions. return null; @@ -414,15 +408,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; checkStateNotNull(eglDisplay); if (frameProcessors.isEmpty()) { - GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); + GlUtil.focusEglSurface( + eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); } else { GlUtil.focusFramebuffer( eglDisplay, eglContext, eglSurface, framebuffers[0], - inputSizes.get(0).getWidth(), - inputSizes.get(0).getHeight()); + inputSize.getWidth(), + inputSize.getHeight()); } inputSurfaceTexture.updateTexImage(); inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); @@ -433,19 +428,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs); for (int i = 0; i < frameProcessors.size() - 1; i++) { - Size outputSize = inputSizes.get(i + 1); + Size intermediateSize = frameProcessors.get(i).getOutputSize(); GlUtil.focusFramebuffer( eglDisplay, eglContext, eglSurface, framebuffers[i + 1], - outputSize.getWidth(), - outputSize.getHeight()); + intermediateSize.getWidth(), + intermediateSize.getHeight()); clearOutputFrame(); frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); } if (!frameProcessors.isEmpty()) { - GlUtil.focusEglSurface(eglDisplay, eglContext, eglSurface, outputWidth, outputHeight); + GlUtil.focusEglSurface( + eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); clearOutputFrame(); getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); } @@ -474,26 +470,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Configures the input and output {@linkplain Size sizes} of a list of {@link GlFrameProcessor * GlFrameProcessors}. - * - * @param inputWidth The width of frames passed to the first {@link GlFrameProcessor}, in pixels. - * @param inputHeight The height of frames passed to the first {@link GlFrameProcessor}, in - * pixels. - * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors}. - * @return The input {@link Size} of each {@link GlFrameProcessor} and the output {@link Size} of - * the final {@link GlFrameProcessor}. */ - private static Pair, Size> configureFrameProcessorSizes( - int inputWidth, int inputHeight, List frameProcessors) { - Size size = new Size(inputWidth, inputHeight); - if (frameProcessors.isEmpty()) { - return Pair.create(ImmutableList.of(size), size); - } - - ImmutableList.Builder inputSizes = new ImmutableList.Builder<>(); + private static void configureFrameProcessorSizes( + Size inputSize, List frameProcessors) { for (int i = 0; i < frameProcessors.size(); i++) { - inputSizes.add(size); - size = frameProcessors.get(i).configureOutputSize(size.getWidth(), size.getHeight()); + frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); + inputSize = frameProcessors.get(i).getOutputSize(); } - return Pair.create(inputSizes.build(), size); } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index ad9099d8f1..c437df536c 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -26,7 +26,7 @@ import java.io.IOException; * *

    *
  1. The constructor, for implementation-specific arguments. - *
  2. {@link #configureOutputSize(int, int)}, to configure based on input dimensions. + *
  3. {@link #setInputSize(int, int)}, to configure based on input dimensions. *
  4. {@link #initialize(int)}, to set up graphics initialization. *
  5. {@link #updateProgramAndDraw(long)}, to process one frame. *
  6. {@link #release()}, upon conclusion of processing. @@ -38,13 +38,24 @@ public interface GlFrameProcessor { // using a placeholder surface until the encoder surface is known. If so, convert // configureOutputSize to a simple getter. + /** + * Sets the input size of frames processed through {@link #updateProgramAndDraw(long)}. + * + *

    This method must be called before {@link #initialize(int)} and does not use OpenGL, as + * calling this method without a current OpenGL context is allowed. + * + *

    After setting the input size, the output size can be obtained using {@link + * #getOutputSize()}. + */ + void setInputSize(int inputWidth, int inputHeight); + /** * Returns the output {@link Size} of frames processed through {@link * #updateProgramAndDraw(long)}. * - *

    This method must be called before {@link #initialize(int)} and does not use OpenGL. + *

    Must call {@link #setInputSize(int, int)} before calling this method. */ - Size configureOutputSize(int inputWidth, int inputHeight); + Size getOutputSize(); /** * Does any initialization necessary such as loading and compiling a GLSL shader programs. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java index 8b0bf17c8d..09103b3a9e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java @@ -84,6 +84,7 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { private int inputWidth; private int inputHeight; private int outputRotationDegrees; + private @MonotonicNonNull Size outputSize; private @MonotonicNonNull Matrix transformationMatrix; /** @@ -106,7 +107,7 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { * *

    Return values may be {@code 0} or {@code 90} degrees. * - *

    This method can only be called after {@link #configureOutputSize(int, int)}. + *

    This method can only be called after {@link #setInputSize(int, int)}. */ public int getOutputRotationDegrees() { checkState(outputRotationDegrees != C.LENGTH_UNSET); @@ -114,7 +115,7 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { } @Override - public Size configureOutputSize(int inputWidth, int inputHeight) { + public void setInputSize(int inputWidth, int inputHeight) { this.inputWidth = inputWidth; this.inputHeight = inputHeight; transformationMatrix = new Matrix(); @@ -136,18 +137,23 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { // TODO(b/201293185): After fragment shader transformations are implemented, put // postRotate in a later GlFrameProcessor. transformationMatrix.postRotate(outputRotationDegrees); - return new Size(displayHeight, displayWidth); + outputSize = new Size(displayHeight, displayWidth); } else { outputRotationDegrees = 0; - return new Size(displayWidth, displayHeight); + outputSize = new Size(displayWidth, displayHeight); } } + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + @Override public void initialize(int inputTexId) throws IOException { checkStateNotNull(transformationMatrix); advancedFrameProcessor = new AdvancedFrameProcessor(context, transformationMatrix); - advancedFrameProcessor.configureOutputSize(inputWidth, inputHeight); + advancedFrameProcessor.setInputSize(inputWidth, inputHeight); advancedFrameProcessor.initialize(inputTexId); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java index 22a59a5156..5ca8730a2b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java @@ -102,6 +102,7 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; private int inputWidth; private int inputHeight; + private @MonotonicNonNull Size outputSize; private @MonotonicNonNull Matrix adjustedTransformationMatrix; /** @@ -125,13 +126,14 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { } @Override - public Size configureOutputSize(int inputWidth, int inputHeight) { + public void setInputSize(int inputWidth, int inputHeight) { this.inputWidth = inputWidth; this.inputHeight = inputHeight; adjustedTransformationMatrix = new Matrix(transformationMatrix); if (transformationMatrix.isIdentity()) { - return new Size(inputWidth, inputHeight); + outputSize = new Size(inputWidth, inputHeight); + return; } float inputAspectRatio = (float) inputWidth / inputHeight; @@ -161,17 +163,19 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { float xScale = (xMax - xMin) / ndcWidthAndHeight; float yScale = (yMax - yMin) / ndcWidthAndHeight; adjustedTransformationMatrix.postScale(1f / xScale, 1f / yScale); + outputSize = new Size(Math.round(inputWidth * xScale), Math.round(inputHeight * yScale)); + } - int outputWidth = Math.round(inputWidth * xScale); - int outputHeight = Math.round(inputHeight * yScale); - return new Size(outputWidth, outputHeight); + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); } @Override public void initialize(int inputTexId) throws IOException { checkStateNotNull(adjustedTransformationMatrix); advancedFrameProcessor = new AdvancedFrameProcessor(context, adjustedTransformationMatrix); - advancedFrameProcessor.configureOutputSize(inputWidth, inputHeight); + advancedFrameProcessor.setInputSize(inputWidth, inputHeight); advancedFrameProcessor.initialize(inputTexId); } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java index 802c4edabe..7ef38b86d5 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java @@ -27,27 +27,28 @@ import org.junit.runner.RunWith; /** * Unit tests for {@link AdvancedFrameProcessor}. * - *

    See {@link AdvancedFrameProcessorPixelTest} for pixel tests testing {@link + *

    See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link * AdvancedFrameProcessor} given a transformation matrix. */ @RunWith(AndroidJUnit4.class) public final class AdvancedFrameProcessorTest { @Test - public void getOutputDimensions_withIdentityMatrix_leavesDimensionsUnchanged() { + public void getOutputSize_withIdentityMatrix_leavesSizeUnchanged() { Matrix identityMatrix = new Matrix(); int inputWidth = 200; int inputHeight = 150; AdvancedFrameProcessor advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), identityMatrix); - Size outputSize = advancedFrameProcessor.configureOutputSize(inputWidth, inputHeight); + advancedFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = advancedFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @Test - public void getOutputDimensions_withTransformationMatrix_leavesDimensionsUnchanged() { + public void getOutputSize_withTransformationMatrix_leavesSizeUnchanged() { Matrix transformationMatrix = new Matrix(); transformationMatrix.postRotate(/* degrees= */ 90); transformationMatrix.postScale(/* sx= */ .5f, /* sy= */ 1.2f); @@ -56,7 +57,8 @@ public final class AdvancedFrameProcessorTest { AdvancedFrameProcessor advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), transformationMatrix); - Size outputSize = advancedFrameProcessor.configureOutputSize(inputWidth, inputHeight); + advancedFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = advancedFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java index 8749225434..a946482271 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java @@ -139,7 +139,10 @@ public final class FrameProcessorChainTest { } @Override - public Size configureOutputSize(int inputWidth, int inputHeight) { + public void setInputSize(int inputWidth, int inputHeight) {} + + @Override + public Size getOutputSize() { return outputSize; } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java index 0703a2468f..3f3047c01c 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java @@ -33,13 +33,14 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public final class PresentationFrameProcessorTest { @Test - public void configureOutputSize_noEditsLandscape_leavesFramesUnchanged() { + public void getOutputSize_noEditsLandscape_leavesFramesUnchanged() { int inputWidth = 200; int inputHeight = 150; PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight); + presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); @@ -47,13 +48,14 @@ public final class PresentationFrameProcessorTest { } @Test - public void configureOutputSize_noEditsSquare_leavesFramesUnchanged() { + public void getOutputSize_noEditsSquare_leavesFramesUnchanged() { int inputWidth = 150; int inputHeight = 150; PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight); + presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); @@ -61,13 +63,14 @@ public final class PresentationFrameProcessorTest { } @Test - public void configureOutputSize_noEditsPortrait_flipsOrientation() { + public void getOutputSize_noEditsPortrait_flipsOrientation() { int inputWidth = 150; int inputHeight = 200; PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight); + presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(90); assertThat(outputSize.getWidth()).isEqualTo(inputHeight); @@ -75,7 +78,7 @@ public final class PresentationFrameProcessorTest { } @Test - public void configureOutputSize_setResolution_changesDimensions() { + public void getOutputSize_setResolution_changesDimensions() { int inputWidth = 200; int inputHeight = 150; int requestedHeight = 300; @@ -84,7 +87,8 @@ public final class PresentationFrameProcessorTest { .setResolution(requestedHeight) .build(); - Size outputSize = presentationFrameProcessor.configureOutputSize(inputWidth, inputHeight); + presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); assertThat(outputSize.getWidth()).isEqualTo(requestedHeight * inputWidth / inputHeight); @@ -96,7 +100,7 @@ public final class PresentationFrameProcessorTest { PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - // configureOutputSize not called before initialize. + // configureOutputSize not called before getOutputRotationDegrees. assertThrows(IllegalStateException.class, presentationFrameProcessor::getOutputRotationDegrees); } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java index 6fd582b74f..5f4683b4a5 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java @@ -34,13 +34,14 @@ import org.junit.runner.RunWith; public final class ScaleToFitFrameProcessorTest { @Test - public void configureOutputSize_noEdits_leavesFramesUnchanged() { + public void getOutputSize_noEdits_leavesFramesUnchanged() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build(); - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); @@ -58,7 +59,7 @@ public final class ScaleToFitFrameProcessorTest { } @Test - public void configureOutputSize_scaleNarrow_decreasesWidth() { + public void getOutputSize_scaleNarrow_decreasesWidth() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = @@ -66,14 +67,15 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ .5f, /* scaleY= */ 1f) .build(); - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(Math.round(inputWidth * .5f)); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @Test - public void configureOutputSize_scaleWide_increasesWidth() { + public void getOutputSize_scaleWide_increasesWidth() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = @@ -81,14 +83,15 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ 2f, /* scaleY= */ 1f) .build(); - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } @Test - public void configureOutputDimensions_scaleTall_increasesHeight() { + public void getOutputSize_scaleTall_increasesHeight() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = @@ -96,14 +99,15 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ 1f, /* scaleY= */ 2f) .build(); - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight * 2); } @Test - public void configureOutputSize_rotate90_swapsDimensions() { + public void getOutputSize_rotate90_swapsDimensions() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = @@ -111,14 +115,15 @@ public final class ScaleToFitFrameProcessorTest { .setRotationDegrees(90) .build(); - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputHeight); assertThat(outputSize.getHeight()).isEqualTo(inputWidth); } @Test - public void configureOutputSize_rotate45_changesDimensions() { + public void getOutputSize_rotate45_changesDimensions() { int inputWidth = 200; int inputHeight = 150; ScaleToFitFrameProcessor scaleToFitFrameProcessor = @@ -127,7 +132,8 @@ public final class ScaleToFitFrameProcessorTest { .build(); long expectedOutputWidthHeight = 247; - Size outputSize = scaleToFitFrameProcessor.configureOutputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight); assertThat(outputSize.getHeight()).isEqualTo(expectedOutputWidthHeight); From 12fc9d1070253490940e733b2f845f58f9073cf1 Mon Sep 17 00:00:00 2001 From: claincly Date: Thu, 31 Mar 2022 14:31:04 +0100 Subject: [PATCH 034/116] Move a commonly used encoder factory mode to util. The encoder factory will be used in other tests. PiperOrigin-RevId: 438552381 --- .../media3/transformer/AndroidTestUtil.java | 30 +++++++++++++++++ .../transformer/mh/TransformationTest.java | 32 ++----------------- 2 files changed, 32 insertions(+), 30 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index 5740f31cb7..b96d962a05 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -19,9 +19,11 @@ import static androidx.media3.common.util.Assertions.checkState; import android.content.Context; import android.os.Build; +import androidx.media3.common.Format; import androidx.media3.common.util.Log; import java.io.File; import java.io.IOException; +import java.util.List; import org.json.JSONException; import org.json.JSONObject; @@ -48,6 +50,34 @@ public final class AndroidTestUtil { return file; } + /** + * A {@link Codec.EncoderFactory} that forces encoding, wrapping {@link DefaultEncoderFactory}. + */ + public static final Codec.EncoderFactory FORCE_ENCODE_ENCODER_FACTORY = + new Codec.EncoderFactory() { + @Override + public Codec createForAudioEncoding(Format format, List allowedMimeTypes) + throws TransformationException { + return Codec.EncoderFactory.DEFAULT.createForAudioEncoding(format, allowedMimeTypes); + } + + @Override + public Codec createForVideoEncoding(Format format, List allowedMimeTypes) + throws TransformationException { + return Codec.EncoderFactory.DEFAULT.createForVideoEncoding(format, allowedMimeTypes); + } + + @Override + public boolean audioNeedsEncoding() { + return true; + } + + @Override + public boolean videoNeedsEncoding() { + return true; + } + }; + /** * Returns a {@link JSONObject} containing device specific details from {@link Build}, including * manufacturer, model, SDK version and build fingerprint. diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java index 67ae26d8f0..f5ca5f962a 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java @@ -21,20 +21,17 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREAS import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING; import android.content.Context; -import androidx.media3.common.Format; import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; -import androidx.media3.transformer.Codec; +import androidx.media3.transformer.AndroidTestUtil; import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.EncoderSelector; -import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationRequest; import androidx.media3.transformer.Transformer; import androidx.media3.transformer.TransformerAndroidTestRunner; import androidx.media3.transformer.VideoEncoderSettings; import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; -import java.util.List; import org.junit.Test; import org.junit.runner.RunWith; @@ -61,32 +58,7 @@ public class TransformationTest { Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = new Transformer.Builder(context) - .setEncoderFactory( - new Codec.EncoderFactory() { - @Override - public Codec createForAudioEncoding(Format format, List allowedMimeTypes) - throws TransformationException { - return Codec.EncoderFactory.DEFAULT.createForAudioEncoding( - format, allowedMimeTypes); - } - - @Override - public Codec createForVideoEncoding(Format format, List allowedMimeTypes) - throws TransformationException { - return Codec.EncoderFactory.DEFAULT.createForVideoEncoding( - format, allowedMimeTypes); - } - - @Override - public boolean audioNeedsEncoding() { - return true; - } - - @Override - public boolean videoNeedsEncoding() { - return true; - } - }) + .setEncoderFactory(AndroidTestUtil.FORCE_ENCODE_ENCODER_FACTORY) .build(); new TransformerAndroidTestRunner.Builder(context, transformer) .setCalculateSsim(true) From 89c4bbec5bbed5884c9a144475c83086ef3f5c6c Mon Sep 17 00:00:00 2001 From: ibaker Date: Thu, 31 Mar 2022 15:07:43 +0100 Subject: [PATCH 035/116] Stabilise HttpDataSource and its nested exceptions PiperOrigin-RevId: 438558981 --- .../datasource/DataSourceException.java | 8 +++-- .../media3/datasource/HttpDataSource.java | 29 +++++++++++++++++-- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java index fa14682255..a2ad3ba6d5 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/DataSourceException.java @@ -21,7 +21,6 @@ import androidx.media3.common.util.UnstableApi; import java.io.IOException; /** Used to specify reason of a DataSource error. */ -@UnstableApi public class DataSourceException extends IOException { /** @@ -29,6 +28,7 @@ public class DataSourceException extends IOException { * {@link #reason} is {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE} in its * cause stack. */ + @UnstableApi public static boolean isCausedByPositionOutOfRange(IOException e) { @Nullable Throwable cause = e; while (cause != null) { @@ -49,7 +49,7 @@ public class DataSourceException extends IOException { * * @deprecated Use {@link PlaybackException#ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE}. */ - @Deprecated + @UnstableApi @Deprecated public static final int POSITION_OUT_OF_RANGE = PlaybackException.ERROR_CODE_IO_READ_POSITION_OUT_OF_RANGE; @@ -65,6 +65,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@PlaybackException.ErrorCode int reason) { this.reason = reason; } @@ -76,6 +77,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@Nullable Throwable cause, @PlaybackException.ErrorCode int reason) { super(cause); this.reason = reason; @@ -88,6 +90,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException(@Nullable String message, @PlaybackException.ErrorCode int reason) { super(message); this.reason = reason; @@ -101,6 +104,7 @@ public class DataSourceException extends IOException { * @param reason Reason of the error, should be one of the {@code ERROR_CODE_IO_*} in {@link * PlaybackException.ErrorCode}. */ + @UnstableApi public DataSourceException( @Nullable String message, @Nullable Throwable cause, diff --git a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java index ddc9518a8e..55afcf273e 100644 --- a/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java +++ b/libraries/datasource/src/main/java/androidx/media3/datasource/HttpDataSource.java @@ -38,12 +38,12 @@ import java.util.List; import java.util.Map; /** An HTTP {@link DataSource}. */ -@UnstableApi public interface HttpDataSource extends DataSource { /** A factory for {@link HttpDataSource} instances. */ interface Factory extends DataSource.Factory { + @UnstableApi @Override HttpDataSource createDataSource(); @@ -59,6 +59,7 @@ public interface HttpDataSource extends DataSource { * @param defaultRequestProperties The default request properties. * @return This factory. */ + @UnstableApi Factory setDefaultRequestProperties(Map defaultRequestProperties); } @@ -67,6 +68,7 @@ public interface HttpDataSource extends DataSource { * a thread safe way to avoid the potential of creating snapshots of an inconsistent or unintended * state. */ + @UnstableApi final class RequestProperties { private final Map requestProperties; @@ -141,6 +143,7 @@ public interface HttpDataSource extends DataSource { } /** Base implementation of {@link Factory} that sets default request properties. */ + @UnstableApi abstract class BaseFactory implements Factory { private final RequestProperties defaultRequestProperties; @@ -209,6 +212,7 @@ public interface HttpDataSource extends DataSource { * Returns a {@code HttpDataSourceException} whose error code is assigned according to the cause * and type. */ + @UnstableApi public static HttpDataSourceException createForIOException( IOException cause, DataSpec dataSpec, @Type int type) { @PlaybackException.ErrorCode int errorCode; @@ -232,7 +236,7 @@ public interface HttpDataSource extends DataSource { } /** The {@link DataSpec} associated with the current connection. */ - public final DataSpec dataSpec; + @UnstableApi public final DataSpec dataSpec; public final @Type int type; @@ -240,6 +244,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #HttpDataSourceException(DataSpec, int, int) * HttpDataSourceException(DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(DataSpec dataSpec, @Type int type) { this(dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -253,6 +258,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( DataSpec dataSpec, @PlaybackException.ErrorCode int errorCode, @Type int type) { super(assignErrorCode(errorCode, type)); @@ -265,6 +271,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(String, DataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, * int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(String message, DataSpec dataSpec, @Type int type) { this(message, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -279,6 +286,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( String message, DataSpec dataSpec, @@ -294,6 +302,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(IOException, DataSpec, * PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException(IOException cause, DataSpec dataSpec, @Type int type) { this(cause, dataSpec, PlaybackException.ERROR_CODE_IO_UNSPECIFIED, type); @@ -308,6 +317,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( IOException cause, DataSpec dataSpec, @@ -323,6 +333,7 @@ public interface HttpDataSource extends DataSource { * HttpDataSourceException(String, IOException, DataSpec, * PlaybackException.ERROR_CODE_IO_UNSPECIFIED, int)}. */ + @UnstableApi @Deprecated public HttpDataSourceException( String message, IOException cause, DataSpec dataSpec, @Type int type) { @@ -339,6 +350,7 @@ public interface HttpDataSource extends DataSource { * PlaybackException.ErrorCode}. * @param type See {@link Type}. */ + @UnstableApi public HttpDataSourceException( String message, @Nullable IOException cause, @@ -366,6 +378,7 @@ public interface HttpDataSource extends DataSource { */ final class CleartextNotPermittedException extends HttpDataSourceException { + @UnstableApi public CleartextNotPermittedException(IOException cause, DataSpec dataSpec) { super( "Cleartext HTTP traffic not permitted. See" @@ -382,6 +395,7 @@ public interface HttpDataSource extends DataSource { public final String contentType; + @UnstableApi public InvalidContentTypeException(String contentType, DataSpec dataSpec) { super( "Invalid content type: " + contentType, @@ -413,6 +427,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec, * byte[])}. */ + @UnstableApi @Deprecated public InvalidResponseCodeException( int responseCode, Map> headerFields, DataSpec dataSpec) { @@ -429,6 +444,7 @@ public interface HttpDataSource extends DataSource { * @deprecated Use {@link #InvalidResponseCodeException(int, String, IOException, Map, DataSpec, * byte[])}. */ + @UnstableApi @Deprecated public InvalidResponseCodeException( int responseCode, @@ -444,6 +460,7 @@ public interface HttpDataSource extends DataSource { /* responseBody= */ Util.EMPTY_BYTE_ARRAY); } + @UnstableApi public InvalidResponseCodeException( int responseCode, @Nullable String responseMessage, @@ -471,12 +488,15 @@ public interface HttpDataSource extends DataSource { * (in order of decreasing priority) the {@code dataSpec}, {@link #setRequestProperty} and the * default parameters set in the {@link Factory}. */ + @UnstableApi @Override long open(DataSpec dataSpec) throws HttpDataSourceException; + @UnstableApi @Override void close() throws HttpDataSourceException; + @UnstableApi @Override int read(byte[] buffer, int offset, int length) throws HttpDataSourceException; @@ -491,6 +511,7 @@ public interface HttpDataSource extends DataSource { * @param name The name of the header field. * @param value The value of the field. */ + @UnstableApi void setRequestProperty(String name, String value); /** @@ -499,17 +520,21 @@ public interface HttpDataSource extends DataSource { * * @param name The name of the header field. */ + @UnstableApi void clearRequestProperty(String name); /** Clears all request headers that were set by {@link #setRequestProperty(String, String)}. */ + @UnstableApi void clearAllRequestProperties(); /** * When the source is open, returns the HTTP response status code associated with the last {@link * #open} call. Otherwise, returns a negative value. */ + @UnstableApi int getResponseCode(); + @UnstableApi @Override Map> getResponseHeaders(); } From 7bbcf1c4c1740476192ad213db07a7c7a0887411 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Mar 2022 20:38:13 +0100 Subject: [PATCH 036/116] Decrease polling rate for IMA Video Ads from 100ms to 200ms PiperOrigin-RevId: 438634901 --- RELEASENOTES.md | 3 +++ .../java/androidx/media3/exoplayer/ima/AdTagLoader.java | 8 +++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index e87ec82783..beaab36463 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -19,6 +19,9 @@ * Rename `DummySurface` to `PlaceHolderSurface`. * Audio: * Use LG AC3 audio decoder advertising non-standard MIME type. +* Ad playback / IMA: + * Decrease ad polling rate from every 100ms to every 200ms, to line up with + Media Rating Council (MRC) recommendations. * Extractors: * Matroska: Parse `DiscardPadding` for Opus tracks. * UI: diff --git a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java index d4176e67fc..ebf18e1cab 100644 --- a/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java +++ b/libraries/exoplayer_ima/src/main/java/androidx/media3/exoplayer/ima/AdTagLoader.java @@ -84,12 +84,14 @@ import java.util.Map; private static final String IMA_SDK_SETTINGS_PLAYER_VERSION = MediaLibraryInfo.VERSION; /** - * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 100 ms is - * the interval recommended by the IMA documentation. + * Interval at which ad progress updates are provided to the IMA SDK, in milliseconds. 200 ms is + * the interval recommended by the Media Rating Council (MRC) for minimum polling of viewable + * video impressions. + * http://www.mediaratingcouncil.org/063014%20Viewable%20Ad%20Impression%20Guideline_Final.pdf. * * @see VideoAdPlayer.VideoAdPlayerCallback */ - private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 100; + private static final int AD_PROGRESS_UPDATE_INTERVAL_MS = 200; /** The value used in {@link VideoProgressUpdate}s to indicate an unset duration. */ private static final long IMA_DURATION_UNSET = -1L; From 21d085f8a971e8e010d12c32e97b3871be30e775 Mon Sep 17 00:00:00 2001 From: olly Date: Thu, 31 Mar 2022 23:18:52 +0100 Subject: [PATCH 037/116] Reading average and peak bitrates from esds boxes. This provides better compatibility with MediaExtractor, which does read these fields; we also need them for being able to mux file contents into another mp4 file. Also, there is a minor refactor included so that we have an actual type for esds box contents instead of a pair. PiperOrigin-RevId: 438673825 --- RELEASENOTES.md | 1 + .../media3/common/util/MediaFormatUtil.java | 14 ++++ .../media3/extractor/mp4/AtomParsers.java | 72 ++++++++++++++----- .../extractordumps/mp4/sample.mp4.0.dump | 1 + .../extractordumps/mp4/sample.mp4.1.dump | 1 + .../extractordumps/mp4/sample.mp4.2.dump | 1 + .../extractordumps/mp4/sample.mp4.3.dump | 1 + .../mp4/sample.mp4.unknown_length.dump | 1 + .../mp4/sample_fragmented.mp4.0.dump | 1 + .../sample_fragmented.mp4.unknown_length.dump | 1 + .../mp4/sample_fragmented_seekable.mp4.0.dump | 2 + .../mp4/sample_fragmented_seekable.mp4.1.dump | 2 + .../mp4/sample_fragmented_seekable.mp4.2.dump | 2 + .../mp4/sample_fragmented_seekable.mp4.3.dump | 2 + ...ragmented_seekable.mp4.unknown_length.dump | 2 + .../mp4/sample_fragmented_sei.mp4.0.dump | 1 + ...ple_fragmented_sei.mp4.unknown_length.dump | 1 + .../mp4/sample_mdat_too_long.mp4.0.dump | 1 + .../mp4/sample_mdat_too_long.mp4.1.dump | 1 + .../mp4/sample_mdat_too_long.mp4.2.dump | 1 + .../mp4/sample_mdat_too_long.mp4.3.dump | 1 + ...mple_mdat_too_long.mp4.unknown_length.dump | 1 + .../sample_partially_fragmented.mp4.0.dump | 2 + ...rtially_fragmented.mp4.unknown_length.dump | 2 + .../mp4/sample_with_color_info.mp4.0.dump | 1 + .../mp4/sample_with_color_info.mp4.1.dump | 1 + .../mp4/sample_with_color_info.mp4.2.dump | 1 + .../mp4/sample_with_color_info.mp4.3.dump | 1 + ...le_with_color_info.mp4.unknown_length.dump | 1 + .../sample_with_colr_mdcv_and_clli.mp4.0.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.1.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.2.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.3.dump | 2 + ...colr_mdcv_and_clli.mp4.unknown_length.dump | 2 + .../transformerdumps/mp4/sample.mp4.dump | 1 + .../mp4/sample.mp4.novideo.dump | 1 + 36 files changed, 116 insertions(+), 16 deletions(-) diff --git a/RELEASENOTES.md b/RELEASENOTES.md index beaab36463..3b8c935c72 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -24,6 +24,7 @@ Media Rating Council (MRC) recommendations. * Extractors: * Matroska: Parse `DiscardPadding` for Opus tracks. + * Parse bitrates from `esds` boxes. * UI: * Fix delivery of events to `OnClickListener`s set on `PlayerView` and `LegacyPlayerView`, in the case that `useController=false` diff --git a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java index 74b244c1b4..6b62f15247 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/MediaFormatUtil.java @@ -47,6 +47,19 @@ public final class MediaFormatUtil { // The constant value must not be changed, because it's also set by the framework MediaParser API. public static final String KEY_PCM_ENCODING_EXTENDED = "exo-pcm-encoding-int"; + /** + * The {@link MediaFormat} key for the maximum bitrate in bits per second. + * + *

    The associated value is an integer. + * + *

    The key string constant is the same as {@code MediaFormat#KEY_MAX_BITRATE}. Values for it + * are already returned by the framework MediaExtractor; the key is a hidden field in {@code + * MediaFormat} though, which is why it's being replicated here. + */ + // The constant value must not be changed, because it's also set by the framework MediaParser and + // MediaExtractor APIs. + public static final String KEY_MAX_BIT_RATE = "max-bitrate"; + private static final int MAX_POWER_OF_TWO_INT = 1 << 30; /** @@ -63,6 +76,7 @@ public final class MediaFormatUtil { public static MediaFormat createMediaFormatFromFormat(Format format) { MediaFormat result = new MediaFormat(); maybeSetInteger(result, MediaFormat.KEY_BIT_RATE, format.bitrate); + maybeSetInteger(result, KEY_MAX_BIT_RATE, format.peakBitrate); maybeSetInteger(result, MediaFormat.KEY_CHANNEL_COUNT, format.channelCount); maybeSetColorInfo(result, format.colorInfo); diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index bd34ba5970..05511fc7e5 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1116,6 +1116,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; @Nullable String codecs = null; @Nullable byte[] projectionData = null; @C.StereoMode int stereoMode = Format.NO_VALUE; + @Nullable EsdsData esdsData = null; // HDR related metadata. @C.ColorSpace int colorSpace = Format.NO_VALUE; @@ -1210,10 +1211,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; mimeType = MimeTypes.VIDEO_H263; } else if (childAtomType == Atom.TYPE_esds) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); - Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationDataBytes = - parseEsdsFromParent(parent, childStartPosition); - mimeType = mimeTypeAndInitializationDataBytes.first; - @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationDataBytes.second; + esdsData = parseEsdsFromParent(parent, childStartPosition); + mimeType = esdsData.mimeType; + @Nullable byte[] initializationDataBytes = esdsData.initializationData; if (initializationDataBytes != null) { initializationData = ImmutableList.of(initializationDataBytes); } @@ -1301,6 +1301,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; colorTransfer, hdrStaticInfo != null ? hdrStaticInfo.array() : null)); } + + if (esdsData != null) { + formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate); + } + out.format = formatBuilder.build(); } @@ -1391,6 +1396,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; int sampleRateMlp = 0; @C.PcmEncoding int pcmEncoding = Format.NO_VALUE; @Nullable String codecs = null; + @Nullable EsdsData esdsData = null; if (quickTimeSoundDescriptionVersion == 0 || quickTimeSoundDescriptionVersion == 1) { channelCount = parent.readUnsignedShort(); @@ -1507,10 +1513,9 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; ? childPosition : findBoxPosition(parent, Atom.TYPE_esds, childPosition, childAtomSize); if (esdsAtomPosition != C.POSITION_UNSET) { - Pair<@NullableType String, byte @NullableType []> mimeTypeAndInitializationData = - parseEsdsFromParent(parent, esdsAtomPosition); - mimeType = mimeTypeAndInitializationData.first; - @Nullable byte[] initializationDataBytes = mimeTypeAndInitializationData.second; + esdsData = parseEsdsFromParent(parent, esdsAtomPosition); + mimeType = esdsData.mimeType; + @Nullable byte[] initializationDataBytes = esdsData.initializationData; if (initializationDataBytes != null) { if (MimeTypes.AUDIO_AAC.equals(mimeType)) { // Update sampleRate and channelCount from the AudioSpecificConfig initialization @@ -1591,7 +1596,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } if (out.format == null && mimeType != null) { - out.format = + Format.Builder formatBuilder = new Format.Builder() .setId(trackId) .setSampleMimeType(mimeType) @@ -1601,8 +1606,13 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; .setPcmEncoding(pcmEncoding) .setInitializationData(initializationData) .setDrmInitData(drmInitData) - .setLanguage(language) - .build(); + .setLanguage(language); + + if (esdsData != null) { + formatBuilder.setAverageBitrate(esdsData.bitrate).setPeakBitrate(esdsData.peakBitrate); + } + + out.format = formatBuilder.build(); } } @@ -1637,8 +1647,7 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } /** Returns codec-specific initialization data contained in an esds box. */ - private static Pair<@NullableType String, byte @NullableType []> parseEsdsFromParent( - ParsableByteArray parent, int position) { + private static EsdsData parseEsdsFromParent(ParsableByteArray parent, int position) { parent.setPosition(position + Atom.HEADER_SIZE + 4); // Start of the ES_Descriptor (defined in ISO/IEC 14496-1) parent.skipBytes(1); // ES_Descriptor tag @@ -1666,17 +1675,29 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; if (MimeTypes.AUDIO_MPEG.equals(mimeType) || MimeTypes.AUDIO_DTS.equals(mimeType) || MimeTypes.AUDIO_DTS_HD.equals(mimeType)) { - return Pair.create(mimeType, null); + return new EsdsData( + mimeType, + /* initializationData= */ null, + /* bitrate= */ Format.NO_VALUE, + /* peakBitrate= */ Format.NO_VALUE); } - parent.skipBytes(12); + parent.skipBytes(4); + int peakBitrate = parent.readUnsignedIntToInt(); + int bitrate = parent.readUnsignedIntToInt(); // Start of the DecoderSpecificInfo. parent.skipBytes(1); // DecoderSpecificInfo tag int initializationDataSize = parseExpandableClassSize(parent); byte[] initializationData = new byte[initializationDataSize]; parent.readBytes(initializationData, 0, initializationDataSize); - return Pair.create(mimeType, initializationData); + + // Skipping zero values as unknown. + return new EsdsData( + mimeType, + /* initializationData= */ initializationData, + /* bitrate= */ bitrate > 0 ? bitrate : Format.NO_VALUE, + /* peakBitrate= */ peakBitrate > 0 ? peakBitrate : Format.NO_VALUE); } /** @@ -1918,6 +1939,25 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } } + /** Data parsed from an esds box. */ + private static final class EsdsData { + private final @NullableType String mimeType; + private final byte @NullableType [] initializationData; + private final int bitrate; + private final int peakBitrate; + + public EsdsData( + @NullableType String mimeType, + byte @NullableType [] initializationData, + int bitrate, + int peakBitrate) { + this.mimeType = mimeType; + this.initializationData = initializationData; + this.bitrate = bitrate; + this.peakBitrate = peakBitrate; + } + } + /** A box containing sample sizes (e.g. stsz, stz2). */ private interface SampleSizeBox { diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump index 78d2922cbf..804961f690 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.0.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump index cbfab8edfd..a974548364 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.1.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 7464 sample count = 33 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump index bd8df24015..19fd0f36d2 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.2.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 4019 sample count = 18 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump index 791aea54f7..80ca2a76c7 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.3.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 470 sample count = 3 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump index 78d2922cbf..804961f690 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample.mp4.unknown_length.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump index 1fca581047..cb3c2003f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.0.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump index 1fca581047..cb3c2003f0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented.mp4.unknown_length.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump index b96cd5335f..5165fb2222 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.0.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump index 508eb65d16..d64a12c6d4 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.1.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 13359 sample count = 31 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump index 4aa40d6145..98cc19fadf 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.2.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 6804 sample count = 16 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump index 5196703b22..3fd459b246 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.3.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 10 sample count = 1 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump index b96cd5335f..5165fb2222 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_seekable.mp4.unknown_length.dump @@ -142,6 +142,8 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + averageBitrate = 136736 + peakBitrate = 145976 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump index 91376322a8..cfc8c3c60c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.0.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump index 91376322a8..cfc8c3c60c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_fragmented_sei.mp4.unknown_length.dump @@ -139,6 +139,7 @@ track 1: total output bytes = 18257 sample count = 46 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump index 0ce12c9a5c..321bc3a832 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.0.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump index 9dc2300bd9..4d8fe681ce 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.1.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 7464 sample count = 33 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump index d1d819d94a..a3e5cd60d0 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.2.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 4019 sample count = 18 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump index 2241b26787..a498d93b5e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.3.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 470 sample count = 3 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump index 0ce12c9a5c..321bc3a832 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_mdat_too_long.mp4.unknown_length.dump @@ -144,6 +144,7 @@ track 1: total output bytes = 9529 sample count = 45 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump index 9e58eea933..85e1af453c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.0.dump @@ -139,6 +139,8 @@ track 1: total output bytes = 10107 sample count = 45 format 0: + averageBitrate = 1254 + peakBitrate = 69000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump index 9e58eea933..85e1af453c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_partially_fragmented.mp4.unknown_length.dump @@ -139,6 +139,8 @@ track 1: total output bytes = 10107 sample count = 45 format 0: + averageBitrate = 1254 + peakBitrate = 69000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump index 5fd303aed3..4696001142 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.0.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 5993 sample count = 15 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump index fa9e2af42e..c8c6fd609a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.1.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 4794 sample count = 11 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump index 8b91ab1a5b..4b825cdc0a 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.2.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 3001 sample count = 7 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump index 22b236d572..0e9f58b51c 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.3.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 1074 sample count = 3 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump index 5fd303aed3..4696001142 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_color_info.mp4.unknown_length.dump @@ -65,6 +65,7 @@ track 1: total output bytes = 5993 sample count = 15 format 0: + peakBitrate = 192000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump index 8ef4f19b16..8c1813ef83 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump @@ -265,6 +265,8 @@ track 1: total output bytes = 16638 sample count = 44 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump index 1e1023afb0..5011cfa353 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump @@ -265,6 +265,8 @@ track 1: total output bytes = 11156 sample count = 30 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump index 5b51396c83..ad7c5fbe40 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump @@ -265,6 +265,8 @@ track 1: total output bytes = 5567 sample count = 15 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump index d66f9234a1..9e8fbd7584 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump @@ -265,6 +265,8 @@ track 1: total output bytes = 374 sample count = 1 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump index 8ef4f19b16..8c1813ef83 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump @@ -265,6 +265,8 @@ track 1: total output bytes = 16638 sample count = 44 format 0: + averageBitrate = 130279 + peakBitrate = 130279 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump index 7b6604be43..a38e2c887e 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.dump @@ -1,5 +1,6 @@ containerMimeType = video/mp4 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 diff --git a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump index adc14a43a1..adbbb3a013 100644 --- a/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump +++ b/libraries/test_data/src/test/assets/transformerdumps/mp4/sample.mp4.novideo.dump @@ -1,5 +1,6 @@ containerMimeType = video/mp4 format 0: + peakBitrate = 200000 id = 2 sampleMimeType = audio/mp4a-latm codecs = mp4a.40.2 From 6d753afc4c3066633a6d2c4ad26477f94b866d9d Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 1 Apr 2022 11:18:25 +0100 Subject: [PATCH 038/116] Store max timestamp rather than last written timestamp per track. This allows the MuxerWrapper to keep using trackTypeToTimeUs for calculating the video duration but slightly changes the meaning of its interleaving constraints. PiperOrigin-RevId: 438780686 --- .../main/java/androidx/media3/transformer/MuxerWrapper.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index 2a0a946910..ce8af780ac 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -160,7 +160,9 @@ import java.nio.ByteBuffer; trackTypeToBytesWritten.put( trackType, trackTypeToBytesWritten.get(trackType) + data.remaining()); - trackTypeToTimeUs.put(trackType, presentationTimeUs); + if (trackTypeToTimeUs.get(trackType) < presentationTimeUs) { + trackTypeToTimeUs.put(trackType, presentationTimeUs); + } muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); previousTrackType = trackType; From fa47233628abb533781865db522d99fb45b9f363 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 1 Apr 2022 13:54:51 +0100 Subject: [PATCH 039/116] Add FrameProcessorChain factory method and make constructor private. PiperOrigin-RevId: 438804850 --- .../FrameProcessorChainPixelTest.java | 2 +- .../transformer/FrameProcessorChain.java | 107 +++++++++--------- .../VideoTranscodingSamplePipeline.java | 2 +- .../transformer/FrameProcessorChainTest.java | 10 +- 4 files changed, 63 insertions(+), 58 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java index 00a9dafa6a..b2aed28c87 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java @@ -246,7 +246,7 @@ public final class FrameProcessorChainPixelTest { int inputWidth = checkNotNull(mediaFormat).getInteger(MediaFormat.KEY_WIDTH); int inputHeight = mediaFormat.getInteger(MediaFormat.KEY_HEIGHT); frameProcessorChain = - new FrameProcessorChain( + FrameProcessorChain.create( context, PIXEL_WIDTH_HEIGHT_RATIO, inputWidth, diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 0dfdcb895e..a0cb50ea7b 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -63,6 +63,49 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; GlUtil.glAssertionsEnabled = true; } + /** + * Creates a new instance. + * + * @param context A {@link Context}. + * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. + * @param inputWidth The input frame width, in pixels. + * @param inputHeight The input frame height, in pixels. + * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. + * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. + * @return A new instance. + * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1. + */ + public static FrameProcessorChain create( + Context context, + float pixelWidthHeightRatio, + int inputWidth, + int inputHeight, + List frameProcessors, + boolean enableExperimentalHdrEditing) + throws TransformationException { + if (pixelWidthHeightRatio != 1.0f) { + // TODO(b/211782176): Consider implementing support for non-square pixels. + throw TransformationException.createForFrameProcessorChain( + new UnsupportedOperationException( + "Transformer's FrameProcessorChain currently does not support frame edits on" + + " non-square pixels. The pixelWidthHeightRatio is: " + + pixelWidthHeightRatio), + TransformationException.ERROR_CODE_GL_INIT_FAILED); + } + + ExternalCopyFrameProcessor externalCopyFrameProcessor = + new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); + externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight); + Size inputSize = externalCopyFrameProcessor.getOutputSize(); + for (int i = 0; i < frameProcessors.size(); i++) { + frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); + inputSize = frameProcessors.get(i).getOutputSize(); + } + + return new FrameProcessorChain( + externalCopyFrameProcessor, frameProcessors, enableExperimentalHdrEditing); + } + private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; private final boolean enableExperimentalHdrEditing; @@ -78,7 +121,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; private volatile boolean releaseRequested; private boolean inputStreamEnded; - private final Size inputSize; /** Wraps the {@link #inputSurfaceTexture}. */ private @MonotonicNonNull Surface inputSurface; /** Associated with an OpenGL external texture. */ @@ -116,48 +158,23 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ private @MonotonicNonNull EGLSurface debugPreviewEglSurface; - /** - * Creates a new instance. - * - * @param context A {@link Context}. - * @param pixelWidthHeightRatio The ratio of width over height, for each pixel. - * @param inputWidth The input frame width, in pixels. - * @param inputHeight The input frame height, in pixels. - * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. - * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. - * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1. - */ - public FrameProcessorChain( - Context context, - float pixelWidthHeightRatio, - int inputWidth, - int inputHeight, + private FrameProcessorChain( + ExternalCopyFrameProcessor externalCopyFrameProcessor, List frameProcessors, - boolean enableExperimentalHdrEditing) - throws TransformationException { - if (pixelWidthHeightRatio != 1.0f) { - // TODO(b/211782176): Consider implementing support for non-square pixels. - throw TransformationException.createForFrameProcessorChain( - new UnsupportedOperationException( - "Transformer's FrameProcessorChain currently does not support frame edits on" - + " non-square pixels. The pixelWidthHeightRatio is: " - + pixelWidthHeightRatio), - TransformationException.ERROR_CODE_GL_INIT_FAILED); - } - - this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; + boolean enableExperimentalHdrEditing) { + this.externalCopyFrameProcessor = externalCopyFrameProcessor; this.frameProcessors = ImmutableList.copyOf(frameProcessors); + this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); futures = new ConcurrentLinkedQueue<>(); pendingFrameCount = new AtomicInteger(); - inputSize = new Size(inputWidth, inputHeight); textureTransformMatrix = new float[16]; - externalCopyFrameProcessor = - new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); framebuffers = new int[frameProcessors.size()]; - configureFrameProcessorSizes(inputSize, frameProcessors); - outputSize = frameProcessors.isEmpty() ? inputSize : getLast(frameProcessors).getOutputSize(); + outputSize = + frameProcessors.isEmpty() + ? externalCopyFrameProcessor.getOutputSize() + : getLast(frameProcessors).getOutputSize(); debugPreviewWidth = C.LENGTH_UNSET; debugPreviewHeight = C.LENGTH_UNSET; } @@ -380,10 +397,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } inputExternalTexId = GlUtil.createExternalTexture(); - externalCopyFrameProcessor.setInputSize(inputSize.getWidth(), inputSize.getHeight()); externalCopyFrameProcessor.initialize(inputExternalTexId); - Size intermediateSize = inputSize; + Size intermediateSize = externalCopyFrameProcessor.getOutputSize(); for (int i = 0; i < frameProcessors.size(); i++) { int inputTexId = GlUtil.createTexture(intermediateSize.getWidth(), intermediateSize.getHeight()); @@ -411,13 +427,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; GlUtil.focusEglSurface( eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); } else { + Size intermediateSize = externalCopyFrameProcessor.getOutputSize(); GlUtil.focusFramebuffer( eglDisplay, eglContext, eglSurface, framebuffers[0], - inputSize.getWidth(), - inputSize.getHeight()); + intermediateSize.getWidth(), + intermediateSize.getHeight()); } inputSurfaceTexture.updateTexImage(); inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); @@ -466,16 +483,4 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); GlUtil.checkGlError(); } - - /** - * Configures the input and output {@linkplain Size sizes} of a list of {@link GlFrameProcessor - * GlFrameProcessors}. - */ - private static void configureFrameProcessorSizes( - Size inputSize, List frameProcessors) { - for (int i = 0; i < frameProcessors.size(); i++) { - frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); - inputSize = frameProcessors.get(i).getOutputSize(); - } - } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 068495d5e2..1f23f8bdd7 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -81,7 +81,7 @@ import org.checkerframework.dataflow.qual.Pure; .setResolution(transformationRequest.outputHeight) .build(); frameProcessorChain = - new FrameProcessorChain( + FrameProcessorChain.create( context, inputFormat.pixelWidthHeightRatio, /* inputWidth= */ decodedWidth, diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java index a946482271..933e7c2ba4 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java @@ -37,11 +37,11 @@ import org.junit.runner.RunWith; public final class FrameProcessorChainTest { @Test - public void construct_withSupportedPixelWidthHeightRatio_completesSuccessfully() + public void create_withSupportedPixelWidthHeightRatio_completesSuccessfully() throws TransformationException { Context context = getApplicationContext(); - new FrameProcessorChain( + FrameProcessorChain.create( context, /* pixelWidthHeightRatio= */ 1, /* inputWidth= */ 200, @@ -51,14 +51,14 @@ public final class FrameProcessorChainTest { } @Test - public void construct_withUnsupportedPixelWidthHeightRatio_throwsException() { + public void create_withUnsupportedPixelWidthHeightRatio_throwsException() { Context context = getApplicationContext(); TransformationException exception = assertThrows( TransformationException.class, () -> - new FrameProcessorChain( + FrameProcessorChain.create( context, /* pixelWidthHeightRatio= */ 2, /* inputWidth= */ 200, @@ -121,7 +121,7 @@ public final class FrameProcessorChainTest { for (Size element : frameProcessorOutputSizes) { frameProcessors.add(new FakeFrameProcessor(element)); } - return new FrameProcessorChain( + return FrameProcessorChain.create( getApplicationContext(), /* pixelWidthHeightRatio= */ 1, inputSize.getWidth(), From ad387378b287f7cb5e956e56ddd94bcfdd9522f3 Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Apr 2022 14:02:35 +0100 Subject: [PATCH 040/116] Add @ContentType IntDef to Util.getAdaptiveMimeTypeForContentType Also add a case for RTSP, otherwise lint complains. PiperOrigin-RevId: 438805903 --- .../src/main/java/androidx/media3/common/C.java | 12 ++++++------ .../main/java/androidx/media3/common/util/Util.java | 5 +++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/C.java b/libraries/common/src/main/java/androidx/media3/common/C.java index 649c648207..24d4801d5c 100644 --- a/libraries/common/src/main/java/androidx/media3/common/C.java +++ b/libraries/common/src/main/java/androidx/media3/common/C.java @@ -732,17 +732,17 @@ public final class C { @Target({FIELD, METHOD, PARAMETER, LOCAL_VARIABLE, TYPE_USE}) @IntDef({TYPE_DASH, TYPE_SS, TYPE_HLS, TYPE_RTSP, TYPE_OTHER}) public @interface ContentType {} - /** Value returned by {@link Util#inferContentType(String)} for DASH manifests. */ + /** Value returned by {@link Util#inferContentType} for DASH manifests. */ @UnstableApi public static final int TYPE_DASH = 0; - /** Value returned by {@link Util#inferContentType(String)} for Smooth Streaming manifests. */ + /** Value returned by {@link Util#inferContentType} for Smooth Streaming manifests. */ @UnstableApi public static final int TYPE_SS = 1; - /** Value returned by {@link Util#inferContentType(String)} for HLS manifests. */ + /** Value returned by {@link Util#inferContentType} for HLS manifests. */ @UnstableApi public static final int TYPE_HLS = 2; - /** Value returned by {@link Util#inferContentType(String)} for RTSP. */ + /** Value returned by {@link Util#inferContentType} for RTSP. */ @UnstableApi public static final int TYPE_RTSP = 3; /** - * Value returned by {@link Util#inferContentType(String)} for files other than DASH, HLS or - * Smooth Streaming manifests, or RTSP URIs. + * Value returned by {@link Util#inferContentType} for files other than DASH, HLS or Smooth + * Streaming manifests, or RTSP URIs. */ @UnstableApi public static final int TYPE_OTHER = 4; diff --git a/libraries/common/src/main/java/androidx/media3/common/util/Util.java b/libraries/common/src/main/java/androidx/media3/common/util/Util.java index a0619d1bfa..d3831ed4d0 100644 --- a/libraries/common/src/main/java/androidx/media3/common/util/Util.java +++ b/libraries/common/src/main/java/androidx/media3/common/util/Util.java @@ -1904,10 +1904,10 @@ public final class Util { /** * Returns the MIME type corresponding to the given adaptive {@link ContentType}, or {@code null} - * if the content type is {@link C#TYPE_OTHER}. + * if the content type is not adaptive. */ @Nullable - public static String getAdaptiveMimeTypeForContentType(int contentType) { + public static String getAdaptiveMimeTypeForContentType(@ContentType int contentType) { switch (contentType) { case C.TYPE_DASH: return MimeTypes.APPLICATION_MPD; @@ -1915,6 +1915,7 @@ public final class Util { return MimeTypes.APPLICATION_M3U8; case C.TYPE_SS: return MimeTypes.APPLICATION_SS; + case C.TYPE_RTSP: case C.TYPE_OTHER: default: return null; From 0c6882867b5a75ff8ea9197a4d55181d63f4c486 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Fri, 1 Apr 2022 14:17:58 +0100 Subject: [PATCH 041/116] Add file logging for skipping instrumentation tests. PiperOrigin-RevId: 438808231 --- .../media3/transformer/AndroidTestUtil.java | 60 +++++++++++++++++-- .../TransformerAndroidTestRunner.java | 24 ++------ .../transformer/mh/TransformationTest.java | 9 ++- 3 files changed, 64 insertions(+), 29 deletions(-) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java index b96d962a05..3fdc29bcd8 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AndroidTestUtil.java @@ -22,6 +22,7 @@ import android.os.Build; import androidx.media3.common.Format; import androidx.media3.common.util.Log; import java.io.File; +import java.io.FileWriter; import java.io.IOException; import java.util.List; import org.json.JSONException; @@ -42,12 +43,22 @@ public final class AndroidTestUtil { public static final String MP4_REMOTE_4K60_PORTRAIT_URI_STRING = "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4"; - /* package */ static File createExternalCacheFile(Context context, String fileName) - throws IOException { - File file = new File(context.getExternalCacheDir(), fileName); - checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); - checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); - return file; + + /** + * Log in logcat and in an analysis file that this test was skipped. + * + *

    Analysis file is a JSON summarising the test, saved to the application cache. + * + *

    The analysis json will contain a {@code skipReason} key, with the reason for skipping the + * test case. + */ + public static void recordTestSkipped(Context context, String testId, String reason) + throws JSONException, IOException { + Log.i(testId, reason); + JSONObject testJson = new JSONObject(); + testJson.put("skipReason", reason); + + writeTestSummaryToFile(context, testId, testJson); } /** @@ -106,5 +117,42 @@ public final class AndroidTestUtil { return exceptionJson; } + /** + * Writes the summary of a test run to the application cache file. + * + *

    The cache filename follows the pattern {@code -result.txt}. + * + * @param context The {@link Context}. + * @param testId A unique identifier for the transformer test run. + * @param testJson A {@link JSONObject} containing a summary of the test run. + */ + /* package */ static void writeTestSummaryToFile( + Context context, String testId, JSONObject testJson) throws IOException, JSONException { + testJson.put("testId", testId).put("device", getDeviceDetailsAsJsonObject()); + + String analysisContents = testJson.toString(/* indentSpaces= */ 2); + + // Log contents as well as writing to file, for easier visibility on individual device testing. + Log.i(testId, analysisContents); + + File analysisFile = createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); + try (FileWriter fileWriter = new FileWriter(analysisFile)) { + fileWriter.write(analysisContents); + } + } + + /** + * Creates a {@link File} of the {@code fileName} in the application cache directory. + * + *

    If a file of that name already exists, it is overwritten. + */ + /* package */ static File createExternalCacheFile(Context context, String fileName) + throws IOException { + File file = new File(context.getExternalCacheDir(), fileName); + checkState(!file.exists() || file.delete(), "Could not delete file: " + file.getAbsolutePath()); + checkState(file.createNewFile(), "Could not create file: " + file.getAbsolutePath()); + return file; + } + private AndroidTestUtil() {} } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java index 043eaa2b84..8fd685129e 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerAndroidTestRunner.java @@ -26,14 +26,12 @@ import androidx.media3.common.util.Log; import androidx.media3.common.util.SystemClock; import androidx.test.platform.app.InstrumentationRegistry; import java.io.File; -import java.io.FileWriter; import java.io.IOException; import java.util.Map; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicReference; import org.checkerframework.checker.nullness.compatqual.NullableType; -import org.json.JSONException; import org.json.JSONObject; /** An android instrumentation test runner for {@link Transformer}. */ @@ -174,7 +172,9 @@ public class TransformerAndroidTestRunner { */ public TransformationTestResult run(String testId, String uriString) throws Exception { JSONObject resultJson = new JSONObject(); - resultJson.put("inputValues", JSONObject.wrap(inputValues)); + if (inputValues != null) { + resultJson.put("inputValues", JSONObject.wrap(inputValues)); + } try { TransformationTestResult transformationTestResult = runInternal(testId, uriString); resultJson.put("transformationResult", transformationTestResult.asJsonObject()); @@ -186,7 +186,7 @@ public class TransformerAndroidTestRunner { resultJson.put("exception", AndroidTestUtil.exceptionAsJsonObject(e)); throw e; } finally { - writeTestSummaryToFile(context, testId, resultJson); + AndroidTestUtil.writeTestSummaryToFile(context, testId, resultJson); } } @@ -303,20 +303,4 @@ public class TransformerAndroidTestRunner { return resultBuilder.build(); } - - private static void writeTestSummaryToFile(Context context, String testId, JSONObject resultJson) - throws IOException, JSONException { - resultJson.put("testId", testId).put("device", AndroidTestUtil.getDeviceDetailsAsJsonObject()); - - String analysisContents = resultJson.toString(/* indentSpaces= */ 2); - - // Log contents as well as writing to file, for easier visibility on individual device testing. - Log.i(TAG_PREFIX + testId, analysisContents); - - File analysisFile = - AndroidTestUtil.createExternalCacheFile(context, /* fileName= */ testId + "-result.txt"); - try (FileWriter fileWriter = new FileWriter(analysisFile)) { - fileWriter.write(analysisContents); - } - } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java index f5ca5f962a..1457584391 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/TransformationTest.java @@ -19,9 +19,9 @@ import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_SEF_URI_STRI import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING; import static androidx.media3.transformer.AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING; +import static androidx.media3.transformer.AndroidTestUtil.recordTestSkipped; import android.content.Context; -import androidx.media3.common.util.Log; import androidx.media3.common.util.Util; import androidx.media3.transformer.AndroidTestUtil; import androidx.media3.transformer.DefaultEncoderFactory; @@ -120,14 +120,17 @@ public class TransformationTest { @Test public void transformSef() throws Exception { String testId = TAG + "_transformSef"; + Context context = ApplicationProvider.getApplicationContext(); if (Util.SDK_INT < 25) { // TODO(b/210593256): Remove test skipping after removing the MediaMuxer dependency. - Log.i(testId, "Skipping on this API version due to lack of muxing support"); + recordTestSkipped( + context, + testId, + /* reason= */ "Skipping on this API version due to lack of muxing support"); return; } - Context context = ApplicationProvider.getApplicationContext(); Transformer transformer = new Transformer.Builder(context) .setTransformationRequest( From af5386cbc1dbf4caadf4f0a155a9a5a64546e039 Mon Sep 17 00:00:00 2001 From: samrobinson Date: Fri, 1 Apr 2022 15:15:24 +0100 Subject: [PATCH 042/116] Add the frame count to TransformationResult. Calculate throughputFps for TransformationTestResult. PiperOrigin-RevId: 438817440 --- .../transformer/FrameCountingMuxer.java | 114 ------------------ .../transformer/TransformationTestResult.java | 17 ++- .../transformer/TransformerEndToEndTest.java | 22 ++-- .../media3/transformer/MuxerWrapper.java | 20 ++- .../transformer/TransformationResult.java | 32 ++++- .../media3/transformer/Transformer.java | 3 +- 6 files changed, 68 insertions(+), 140 deletions(-) delete mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java deleted file mode 100644 index 27e05ace84..0000000000 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameCountingMuxer.java +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import android.os.ParcelFileDescriptor; -import androidx.annotation.Nullable; -import androidx.media3.common.C; -import androidx.media3.common.Format; -import androidx.media3.common.MimeTypes; -import com.google.common.collect.ImmutableList; -import java.io.IOException; -import java.nio.ByteBuffer; -import org.checkerframework.checker.nullness.qual.MonotonicNonNull; - -/** - * An implementation of {@link Muxer} that forwards operations to another {@link Muxer}, counting - * the number of frames as they go past. - */ -/* package */ final class FrameCountingMuxer implements Muxer { - public static final class Factory implements Muxer.Factory { - - private final Muxer.Factory muxerFactory; - private @MonotonicNonNull FrameCountingMuxer frameCountingMuxer; - - public Factory(Muxer.Factory muxerFactory) { - this.muxerFactory = muxerFactory; - } - - @Override - public Muxer create(String path, String outputMimeType) throws IOException { - frameCountingMuxer = new FrameCountingMuxer(muxerFactory.create(path, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public Muxer create(ParcelFileDescriptor parcelFileDescriptor, String outputMimeType) - throws IOException { - frameCountingMuxer = - new FrameCountingMuxer(muxerFactory.create(parcelFileDescriptor, outputMimeType)); - return frameCountingMuxer; - } - - @Override - public boolean supportsOutputMimeType(String mimeType) { - return muxerFactory.supportsOutputMimeType(mimeType); - } - - @Override - public boolean supportsSampleMimeType(@Nullable String sampleMimeType, String outputMimeType) { - return muxerFactory.supportsSampleMimeType(sampleMimeType, outputMimeType); - } - - @Override - public ImmutableList getSupportedSampleMimeTypes( - @C.TrackType int trackType, String containerMimeType) { - return muxerFactory.getSupportedSampleMimeTypes(trackType, containerMimeType); - } - - @Nullable - public FrameCountingMuxer getLastFrameCountingMuxerCreated() { - return frameCountingMuxer; - } - } - - private final Muxer muxer; - private int videoTrackIndex; - private int frameCount; - - private FrameCountingMuxer(Muxer muxer) throws IOException { - this.muxer = muxer; - } - - @Override - public int addTrack(Format format) throws MuxerException { - int trackIndex = muxer.addTrack(format); - if (MimeTypes.isVideo(format.sampleMimeType)) { - videoTrackIndex = trackIndex; - } - return trackIndex; - } - - @Override - public void writeSampleData( - int trackIndex, ByteBuffer data, boolean isKeyFrame, long presentationTimeUs) - throws MuxerException { - muxer.writeSampleData(trackIndex, data, isKeyFrame, presentationTimeUs); - if (trackIndex == videoTrackIndex) { - frameCount++; - } - } - - @Override - public void release(boolean forCancellation) throws MuxerException { - muxer.release(forCancellation); - } - - /* Returns the number of frames written for the video track. */ - public int getFrameCount() { - return frameCount; - } -} diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java index d74e5484d9..c2bd3241e2 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformationTestResult.java @@ -31,7 +31,6 @@ public class TransformationTestResult { @Nullable private String filePath; @Nullable private Exception analysisException; - private long elapsedTimeMs; private double ssim; @@ -103,8 +102,12 @@ public class TransformationTestResult { } public final TransformationResult transformationResult; - @Nullable public final String filePath; + /** + * The average rate (per second) at which frames are processed by the transformer, or {@link + * C#RATE_UNSET} if unset or unknown. + */ + public final float throughputFps; /** * The amount of time taken to perform the transformation in milliseconds. {@link C#TIME_UNSET} if * unset. @@ -133,6 +136,12 @@ public class TransformationTestResult { if (transformationResult.averageVideoBitrate != C.RATE_UNSET_INT) { jsonObject.put("averageVideoBitrate", transformationResult.averageVideoBitrate); } + if (transformationResult.videoFrameCount > 0) { + jsonObject.put("videoFrameCount", transformationResult.videoFrameCount); + } + if (throughputFps != C.RATE_UNSET) { + jsonObject.put("throughputFps", throughputFps); + } if (elapsedTimeMs != C.TIME_UNSET) { jsonObject.put("elapsedTimeMs", elapsedTimeMs); } @@ -156,5 +165,9 @@ public class TransformationTestResult { this.elapsedTimeMs = elapsedTimeMs; this.ssim = ssim; this.analysisException = analysisException; + this.throughputFps = + elapsedTimeMs != C.TIME_UNSET && transformationResult.videoFrameCount > 0 + ? 1000f * transformationResult.videoFrameCount / elapsedTimeMs + : C.RATE_UNSET; } } diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java index cdf4e0d902..d1a9182e87 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -15,7 +15,7 @@ */ package androidx.media3.transformer; -import static androidx.media3.common.util.Assertions.checkNotNull; +import static androidx.media3.transformer.AndroidTestUtil.MP4_ASSET_URI_STRING; import static com.google.common.truth.Truth.assertThat; import android.content.Context; @@ -31,18 +31,13 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) public class TransformerEndToEndTest { - private static final String AVC_VIDEO_URI_STRING = "asset:///media/mp4/sample.mp4"; - @Test public void videoEditing_completesWithConsistentFrameCount() throws Exception { Context context = ApplicationProvider.getApplicationContext(); - FrameCountingMuxer.Factory muxerFactory = - new FrameCountingMuxer.Factory(new FrameworkMuxer.Factory()); Transformer transformer = new Transformer.Builder(context) .setTransformationRequest( new TransformationRequest.Builder().setResolution(480).build()) - .setMuxerFactory(muxerFactory) .setEncoderFactory( new DefaultEncoderFactory(EncoderSelector.DEFAULT, /* enableFallback= */ false)) .build(); @@ -50,13 +45,14 @@ public class TransformerEndToEndTest { // ffprobe -count_frames -select_streams v:0 -show_entries stream=nb_read_frames sample.mp4 int expectedFrameCount = 30; - new TransformerAndroidTestRunner.Builder(context, transformer) - .build() - .run(/* testId= */ "videoEditing_completesWithConsistentFrameCount", AVC_VIDEO_URI_STRING); + TransformationTestResult result = + new TransformerAndroidTestRunner.Builder(context, transformer) + .build() + .run( + /* testId= */ "videoEditing_completesWithConsistentFrameCount", + MP4_ASSET_URI_STRING); - FrameCountingMuxer frameCountingMuxer = - checkNotNull(muxerFactory.getLastFrameCountingMuxerCreated()); - assertThat(frameCountingMuxer.getFrameCount()).isEqualTo(expectedFrameCount); + assertThat(result.transformationResult.videoFrameCount).isEqualTo(expectedFrameCount); } @Test @@ -75,7 +71,7 @@ public class TransformerEndToEndTest { TransformationTestResult result = new TransformerAndroidTestRunner.Builder(context, transformer) .build() - .run(/* testId= */ "videoOnly_completesWithConsistentDuration", AVC_VIDEO_URI_STRING); + .run(/* testId= */ "videoOnly_completesWithConsistentDuration", MP4_ASSET_URI_STRING); assertThat(result.transformationResult.durationMs).isEqualTo(expectedDurationMs); } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java index ce8af780ac..97941b5ccf 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/MuxerWrapper.java @@ -48,6 +48,7 @@ import java.nio.ByteBuffer; private final Muxer muxer; private final Muxer.Factory muxerFactory; private final SparseIntArray trackTypeToIndex; + private final SparseIntArray trackTypeToSampleCount; private final SparseLongArray trackTypeToTimeUs; private final SparseLongArray trackTypeToBytesWritten; private final String containerMimeType; @@ -62,7 +63,9 @@ import java.nio.ByteBuffer; this.muxer = muxer; this.muxerFactory = muxerFactory; this.containerMimeType = containerMimeType; + trackTypeToIndex = new SparseIntArray(); + trackTypeToSampleCount = new SparseIntArray(); trackTypeToTimeUs = new SparseLongArray(); trackTypeToBytesWritten = new SparseLongArray(); previousTrackType = C.TRACK_TYPE_NONE; @@ -123,6 +126,7 @@ import java.nio.ByteBuffer; int trackIndex = muxer.addTrack(format); trackTypeToIndex.put(trackType, trackIndex); + trackTypeToSampleCount.put(trackType, 0); trackTypeToTimeUs.put(trackType, 0L); trackTypeToBytesWritten.put(trackType, 0L); trackFormatCount++; @@ -158,6 +162,7 @@ import java.nio.ByteBuffer; return false; } + trackTypeToSampleCount.put(trackType, trackTypeToSampleCount.get(trackType) + 1); trackTypeToBytesWritten.put( trackType, trackTypeToBytesWritten.get(trackType) + data.remaining()); if (trackTypeToTimeUs.get(trackType) < presentationTimeUs) { @@ -218,6 +223,16 @@ import java.nio.ByteBuffer; /* divisor= */ trackDurationUs); } + /** Returns the number of samples written to the track of the provided {@code trackType}. */ + public int getTrackSampleCount(@C.TrackType int trackType) { + return trackTypeToSampleCount.get(trackType, /* valueIfKeyNotFound= */ 0); + } + + /** Returns the duration of the longest track in milliseconds. */ + public long getDurationMs() { + return Util.usToMs(maxValue(trackTypeToTimeUs)); + } + /** * Returns whether the muxer can write a sample of the given track type. * @@ -243,9 +258,4 @@ import java.nio.ByteBuffer; } return trackTimeUs - minTrackTimeUs <= MAX_TRACK_WRITE_AHEAD_US; } - - /** Returns the duration of the longest track in milliseconds. */ - public long getDurationMs() { - return Util.usToMs(maxValue(trackTypeToTimeUs)); - } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java index 2bf98abe1d..7aba68e647 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/TransformationResult.java @@ -31,6 +31,7 @@ public final class TransformationResult { private long fileSizeBytes; private int averageAudioBitrate; private int averageVideoBitrate; + private int videoFrameCount; public Builder() { durationMs = C.TIME_UNSET; @@ -83,13 +84,24 @@ public final class TransformationResult { return this; } + /** + * Sets the number of video frames. + * + *

    Input must be positive or {@code 0}. + */ + public Builder setVideoFrameCount(int videoFrameCount) { + checkArgument(videoFrameCount >= 0); + this.videoFrameCount = videoFrameCount; + return this; + } + public TransformationResult build() { return new TransformationResult( - durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate); + durationMs, fileSizeBytes, averageAudioBitrate, averageVideoBitrate, videoFrameCount); } } - /** The duration of the video in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ + /** The duration of the file in milliseconds, or {@link C#TIME_UNSET} if unset or unknown. */ public final long durationMs; /** The size of the file in bytes, or {@link C#LENGTH_UNSET} if unset or unknown. */ public final long fileSizeBytes; @@ -101,13 +113,20 @@ public final class TransformationResult { * The average bitrate of the video track data, or {@link C#RATE_UNSET_INT} if unset or unknown. */ public final int averageVideoBitrate; + /** The number of video frames. */ + public final int videoFrameCount; private TransformationResult( - long durationMs, long fileSizeBytes, int averageAudioBitrate, int averageVideoBitrate) { + long durationMs, + long fileSizeBytes, + int averageAudioBitrate, + int averageVideoBitrate, + int videoFrameCount) { this.durationMs = durationMs; this.fileSizeBytes = fileSizeBytes; this.averageAudioBitrate = averageAudioBitrate; this.averageVideoBitrate = averageVideoBitrate; + this.videoFrameCount = videoFrameCount; } public Builder buildUpon() { @@ -115,7 +134,8 @@ public final class TransformationResult { .setDurationMs(durationMs) .setFileSizeBytes(fileSizeBytes) .setAverageAudioBitrate(averageAudioBitrate) - .setAverageVideoBitrate(averageVideoBitrate); + .setAverageVideoBitrate(averageVideoBitrate) + .setVideoFrameCount(videoFrameCount); } @Override @@ -130,7 +150,8 @@ public final class TransformationResult { return durationMs == result.durationMs && fileSizeBytes == result.fileSizeBytes && averageAudioBitrate == result.averageAudioBitrate - && averageVideoBitrate == result.averageVideoBitrate; + && averageVideoBitrate == result.averageVideoBitrate + && videoFrameCount == result.videoFrameCount; } @Override @@ -139,6 +160,7 @@ public final class TransformationResult { result = 31 * result + (int) fileSizeBytes; result = 31 * result + averageAudioBitrate; result = 31 * result + averageVideoBitrate; + result = 31 * result + videoFrameCount; return result; } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java index 28e7c4f5c3..c812cdab23 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/Transformer.java @@ -704,7 +704,6 @@ public final class Transformer { if (player != null) { throw new IllegalStateException("There is already a transformation in progress."); } - MuxerWrapper muxerWrapper = new MuxerWrapper(muxer, muxerFactory, containerMimeType); this.muxerWrapper = muxerWrapper; DefaultTrackSelector trackSelector = new DefaultTrackSelector(context); @@ -1005,7 +1004,9 @@ public final class Transformer { .setDurationMs(muxerWrapper.getDurationMs()) .setAverageAudioBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_AUDIO)) .setAverageVideoBitrate(muxerWrapper.getTrackAverageBitrate(C.TRACK_TYPE_VIDEO)) + .setVideoFrameCount(muxerWrapper.getTrackSampleCount(C.TRACK_TYPE_VIDEO)) .build(); + listeners.queueEvent( /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onTransformationCompleted(mediaItem, result)); From 166f04d5e920dd630f11698aa75690695101c92b Mon Sep 17 00:00:00 2001 From: ibaker Date: Fri, 1 Apr 2022 17:19:49 +0100 Subject: [PATCH 043/116] Switch Media3's bug template to a form This is based on the ExoPlayer form: https://github.com/google/ExoPlayer/issues/new?assignees=&labels=bug%2Cneeds+triage&template=bug.yml Also force people to use one of the templates. PiperOrigin-RevId: 438840002 --- .github/ISSUE_TEMPLATE/bug.md | 42 ------------- .github/ISSUE_TEMPLATE/bug.yml | 99 +++++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 1 + 3 files changed, 100 insertions(+), 42 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug.md create mode 100644 .github/ISSUE_TEMPLATE/bug.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md deleted file mode 100644 index 25383cd8dd..0000000000 --- a/.github/ISSUE_TEMPLATE/bug.md +++ /dev/null @@ -1,42 +0,0 @@ ---- -name: Bug report -about: Issue template for a bug report. -title: '' -labels: bug, needs triage -assignees: '' ---- - -We can only process bug reports that are actionable. Unclear bug reports or -reports with insufficient information may not get attention. - -Before filing a bug: -------------------------- - -- Search existing issues, including issues that are closed: - https://github.com/androidx/media/issues?q=is%3Aissue -- For ExoPlayer-related bugs, please also check the ExoPlayer tracker: - https://github.com/google/ExoPlayer/issues?q=is%3Aissue - -When reporting a bug: -------------------------- - -Describe how the issue can be reproduced, ideally using one of the demo apps -or a small sample app that you’re able to share as source code on GitHub. To -increase the chance of your issue getting attention, please also include: - -- Clear reproduction steps including observed and expected behavior -- Output of running "adb bugreport" in the console shortly after encountering - the issue -- URI to test content for reproduction -- For protected content: - - DRM scheme and license server URL - - Authentication HTTP headers - -- AndroidX Media version number -- Android version -- Android device - -If there's something you don't want to post publicly, please submit the issue, -then email the link/bug report to dev.exoplayer@gmail.com using a subject in the -format "Issue #1234", where #1234 is your issue number (we don't reply to -emails). diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml new file mode 100644 index 0000000000..41d4528ced --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -0,0 +1,99 @@ +name: Bug Report +description: Report a bug in the Media3 library +labels: ["bug", "needs triage"] +body: + - type: markdown + attributes: + value: | + We can only process bug reports that are actionable. Unclear bug reports or reports with insufficient information may not get attention. + + Before filing a bug: + ------------------------- + + - Search existing issues, including issues that are closed: https://github.com/androidx/media/issues?q=is%3Aissue + - For ExoPlayer-related bugs, please also check the ExoPlayer tracker: https://github.com/google/ExoPlayer/issues?q=is%3Aissue + - type: dropdown + attributes: + label: Media3 Version + description: What version of Media3 are you using? + options: + - 1.0.0-alpha03 + - 1.0.0-alpha02 + - 1.0.0-alpha01 + validations: + required: true + - type: textarea + attributes: + label: Devices that reproduce the issue + placeholder: | + Example: + * Pixel 4 running Android 12 + * Samsung S21 running Android 11 + validations: + required: true + - type: textarea + attributes: + label: Devices that do not reproduce the issue + placeholder: | + Example: + * Pixel 3 running Android Pie + - type: dropdown + attributes: + label: Reproducible in the demo app? + description: Please try and reproduce the issue in the [Media3 demo app](https://github.com/androidx/media/tree/release/demos/main). + options: + - "Yes" + - "No" + - Not tested + validations: + required: true + - type: textarea + attributes: + label: Reproduction steps + description: Clear and complete steps we can use to reproduce the problem + placeholder: | + Example: + 1. Play the attached media in the demo app + 2. Seek forward 10s + validations: + required: true + - type: textarea + attributes: + label: Expected result + placeholder: | + Example: + The media plays successfully + validations: + required: true + - type: textarea + attributes: + label: Actual result + placeholder: | + Example: + Playback crashes with the following stack trace: + ... + validations: + required: true + - type: textarea + attributes: + label: Media + description: | + Media we can use to reproduce the problem. Either: + * Attach a file here + * Include a media URL + * Refer to a piece of media from the demo app (e.g. `Misc > Dizzy (MP4)`) + * If you don't want to post media publicly please email the info to dev.exoplayer@gmail.com with subject 'Issue #\' after filing this issue, and note that you will do this here. + * If you are certain the issue does not depend on the media being played, enter "Not applicable" here. + + For DRM-protected media please also include the scheme and license server URL. + validations: + required: true + - type: checkboxes + attributes: + label: Bug Report + description: | + After filing this issue please run `adb bugreport` shortly after reproducing the problem (ideally in the [demo app](https://github.com/androidx/media/tree/release/demos/main)) to capture a zip file, and email this to dev.exoplayer@gmail.com with subject 'Issue #\'. + + **Note:** Logcat output is **not** the same as a full bug report, and is often missing information that's useful for diagnosing issues. Please ensure you're sending a full bug report zip file. + options: + - label: You will email the zip file produced by `adb bugreport` to dev.exoplayer@gmail.com after filing this issue. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000..3ba13e0cec --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1 @@ +blank_issues_enabled: false From f8f8b75500ffe28367358d1ff8474bab2a589860 Mon Sep 17 00:00:00 2001 From: olly Date: Fri, 1 Apr 2022 17:24:25 +0100 Subject: [PATCH 044/116] Exclude TrackGroup fields/methods from the stable API App code should get all of this information from TrackGroupInfo, and should only need TrackGroup as a key to use for overrides. PiperOrigin-RevId: 438840925 --- .../src/main/java/androidx/media3/common/TrackGroup.java | 8 +++++--- .../java/androidx/media3/exoplayer/util/EventLogger.java | 8 +++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java index dc9867ee30..ecca4c3151 100644 --- a/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java +++ b/libraries/common/src/main/java/androidx/media3/common/TrackGroup.java @@ -54,11 +54,11 @@ public final class TrackGroup implements Bundleable { private static final String TAG = "TrackGroup"; /** The number of tracks in the group. */ - public final int length; + @UnstableApi public final int length; /** An identifier for the track group. */ - public final String id; + @UnstableApi public final String id; /** The type of tracks in the group. */ - public final @C.TrackType int type; + @UnstableApi public final @C.TrackType int type; private final Format[] formats; @@ -113,6 +113,7 @@ public final class TrackGroup implements Bundleable { * @param index The index of the track. * @return The track's format. */ + @UnstableApi public Format getFormat(int index) { return formats[index]; } @@ -126,6 +127,7 @@ public final class TrackGroup implements Bundleable { * @return The index of the track, or {@link C#INDEX_UNSET} if no such track exists. */ @SuppressWarnings("ReferenceEquality") + @UnstableApi public int indexOf(Format format) { for (int i = 0; i < formats.length; i++) { if (format == formats[i]) { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java index 758acbedea..f84eedabc9 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/util/EventLogger.java @@ -31,7 +31,6 @@ import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; -import androidx.media3.common.TrackGroup; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; import androidx.media3.common.util.Log; @@ -288,11 +287,10 @@ public class EventLogger implements AnalyticsListener { // Log metadata for at most one of the selected tracks. boolean loggedMetadata = false; for (int groupIndex = 0; !loggedMetadata && groupIndex < trackGroupInfos.size(); groupIndex++) { - TracksInfo.TrackGroupInfo trackGroupInfo = trackGroupInfos.get(groupIndex); - TrackGroup trackGroup = trackGroupInfo.getTrackGroup(); + TracksInfo.TrackGroupInfo trackGroup = trackGroupInfos.get(groupIndex); for (int trackIndex = 0; !loggedMetadata && trackIndex < trackGroup.length; trackIndex++) { - if (trackGroupInfo.isTrackSelected(trackIndex)) { - @Nullable Metadata metadata = trackGroup.getFormat(trackIndex).metadata; + if (trackGroup.isTrackSelected(trackIndex)) { + @Nullable Metadata metadata = trackGroup.getTrackFormat(trackIndex).metadata; if (metadata != null && metadata.length() > 0) { logd(" Metadata ["); printMetadata(metadata, " "); From 2a66c7b8f5e4ca95a585e5f90080b3fa0c6788fa Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 1 Apr 2022 17:41:01 +0100 Subject: [PATCH 045/116] Move FrameProcessorChain OpenGL setup to factory method. The encoder surface is no longer needed for the OpenGL setup and frame processor initialization, as a placeholder surface is used instead. So all of the setup can now be done in the factory method. PiperOrigin-RevId: 438844450 --- .../FrameProcessorChainPixelTest.java | 2 +- .../transformer/FrameProcessorChainTest.java | 5 +- .../transformer/FrameProcessorChain.java | 218 +++++++++--------- .../VideoTranscodingSamplePipeline.java | 2 +- .../DefaultEncoderFactoryTest.java | 20 +- .../transformer/TransformerEndToEndTest.java | 26 --- 6 files changed, 129 insertions(+), 144 deletions(-) rename libraries/transformer/src/{test => androidTest}/java/androidx/media3/transformer/FrameProcessorChainTest.java (97%) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java index b2aed28c87..78df45a3ad 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainPixelTest.java @@ -260,7 +260,7 @@ public final class FrameProcessorChainPixelTest { outputSize.getHeight(), PixelFormat.RGBA_8888, /* maxImages= */ 1); - frameProcessorChain.configure( + frameProcessorChain.setOutputSurface( outputImageReader.getSurface(), outputSize.getWidth(), outputSize.getHeight(), diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java similarity index 97% rename from libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java rename to libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java index 933e7c2ba4..051eb3aefe 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/FrameProcessorChainTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java @@ -28,10 +28,9 @@ import org.junit.Test; import org.junit.runner.RunWith; /** - * Robolectric tests for {@link FrameProcessorChain}. + * Tests for creating and configuring a {@link FrameProcessorChain}. * - *

    See {@code FrameProcessorChainPixelTest} in the androidTest directory for instrumentation - * tests. + *

    See {@link FrameProcessorChainPixelTest} for data processing tests. */ @RunWith(AndroidJUnit4.class) public final class FrameProcessorChainTest { diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index a0cb50ea7b..3199ed5b96 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -54,7 +54,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * and is processed on a background thread as it becomes available. All input frames should be * {@linkplain #registerInputFrame() registered} before they are rendered to the input surface. * {@link #getPendingFrameCount()} can be used to check whether there are frames that have not been - * fully processed yet. Output is written to its {@linkplain #configure(Surface, int, int, + * fully processed yet. Output is written to its {@linkplain #setOutputSurface(Surface, int, int, * SurfaceView) output surface}. */ /* package */ final class FrameProcessorChain { @@ -73,7 +73,9 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; * @param frameProcessors The {@link GlFrameProcessor GlFrameProcessors} to apply to each frame. * @param enableExperimentalHdrEditing Whether to attempt to process the input as an HDR signal. * @return A new instance. - * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1. + * @throws TransformationException If the {@code pixelWidthHeightRatio} isn't 1, reading shader + * files fails, or an OpenGL error occurs while creating and configuring the OpenGL + * components. */ public static FrameProcessorChain create( Context context, @@ -93,42 +95,104 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; TransformationException.ERROR_CODE_GL_INIT_FAILED); } + ExecutorService singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); ExternalCopyFrameProcessor externalCopyFrameProcessor = new ExternalCopyFrameProcessor(context, enableExperimentalHdrEditing); - externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight); - Size inputSize = externalCopyFrameProcessor.getOutputSize(); - for (int i = 0; i < frameProcessors.size(); i++) { - frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); - inputSize = frameProcessors.get(i).getOutputSize(); + + try { + return singleThreadExecutorService + .submit( + () -> + createOpenGlObjectsAndFrameProcessorChain( + inputWidth, + inputHeight, + frameProcessors, + enableExperimentalHdrEditing, + singleThreadExecutorService, + externalCopyFrameProcessor)) + .get(); + } catch (ExecutionException e) { + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_INIT_FAILED); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw TransformationException.createForFrameProcessorChain( + e, TransformationException.ERROR_CODE_GL_INIT_FAILED); + } + } + + /** + * Creates the OpenGL textures, framebuffers, initializes the {@link GlFrameProcessor + * GlFrameProcessors} and returns a new {@code FrameProcessorChain}. + * + *

    This method must by executed using the {@code singleThreadExecutorService}. + */ + private static FrameProcessorChain createOpenGlObjectsAndFrameProcessorChain( + int inputWidth, + int inputHeight, + List frameProcessors, + boolean enableExperimentalHdrEditing, + ExecutorService singleThreadExecutorService, + ExternalCopyFrameProcessor externalCopyFrameProcessor) + throws IOException { + checkState(Thread.currentThread().getName().equals(THREAD_NAME)); + + EGLDisplay eglDisplay = GlUtil.createEglDisplay(); + EGLContext eglContext = + enableExperimentalHdrEditing + ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) + : GlUtil.createEglContext(eglDisplay); + + if (GlUtil.isSurfacelessContextExtensionSupported()) { + GlUtil.focusEglSurface( + eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); + } else if (enableExperimentalHdrEditing) { + // TODO(b/209404935): Don't assume BT.2020 PQ input/output. + GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay); + } else { + GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); } + int inputExternalTexId = GlUtil.createExternalTexture(); + externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight); + externalCopyFrameProcessor.initialize(inputExternalTexId); + + int[] framebuffers = new int[frameProcessors.size()]; + Size inputSize = externalCopyFrameProcessor.getOutputSize(); + for (int i = 0; i < frameProcessors.size(); i++) { + int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight()); + framebuffers[i] = GlUtil.createFboForTexture(inputTexId); + frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); + frameProcessors.get(i).initialize(inputTexId); + inputSize = frameProcessors.get(i).getOutputSize(); + } return new FrameProcessorChain( - externalCopyFrameProcessor, frameProcessors, enableExperimentalHdrEditing); + eglDisplay, + eglContext, + singleThreadExecutorService, + inputExternalTexId, + externalCopyFrameProcessor, + framebuffers, + ImmutableList.copyOf(frameProcessors), + enableExperimentalHdrEditing); } private static final String THREAD_NAME = "Transformer:FrameProcessorChain"; private final boolean enableExperimentalHdrEditing; - private @MonotonicNonNull EGLDisplay eglDisplay; - private @MonotonicNonNull EGLContext eglContext; + private final EGLDisplay eglDisplay; + private final EGLContext eglContext; /** Some OpenGL commands may block, so all OpenGL commands are run on a background thread. */ private final ExecutorService singleThreadExecutorService; /** Futures corresponding to the executor service's pending tasks. */ private final ConcurrentLinkedQueue> futures; /** Number of frames {@linkplain #registerInputFrame() registered} but not fully processed. */ private final AtomicInteger pendingFrameCount; - /** Prevents further frame processing tasks from being scheduled after {@link #release()}. */ - private volatile boolean releaseRequested; - private boolean inputStreamEnded; /** Wraps the {@link #inputSurfaceTexture}. */ - private @MonotonicNonNull Surface inputSurface; + private final Surface inputSurface; /** Associated with an OpenGL external texture. */ - private @MonotonicNonNull SurfaceTexture inputSurfaceTexture; - /** - * Identifier of the external texture the {@link ExternalCopyFrameProcessor} reads its input from. - */ - private int inputExternalTexId; + private final SurfaceTexture inputSurfaceTexture; /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */ private final float[] textureTransformMatrix; @@ -158,19 +222,33 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; */ private @MonotonicNonNull EGLSurface debugPreviewEglSurface; + private boolean inputStreamEnded; + /** Prevents further frame processing tasks from being scheduled after {@link #release()}. */ + private volatile boolean releaseRequested; + private FrameProcessorChain( + EGLDisplay eglDisplay, + EGLContext eglContext, + ExecutorService singleThreadExecutorService, + int inputExternalTexId, ExternalCopyFrameProcessor externalCopyFrameProcessor, - List frameProcessors, + int[] framebuffers, + ImmutableList frameProcessors, boolean enableExperimentalHdrEditing) { + + this.eglDisplay = eglDisplay; + this.eglContext = eglContext; + this.singleThreadExecutorService = singleThreadExecutorService; this.externalCopyFrameProcessor = externalCopyFrameProcessor; - this.frameProcessors = ImmutableList.copyOf(frameProcessors); + this.framebuffers = framebuffers; + this.frameProcessors = frameProcessors; this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; - singleThreadExecutorService = Util.newSingleThreadExecutor(THREAD_NAME); futures = new ConcurrentLinkedQueue<>(); pendingFrameCount = new AtomicInteger(); + inputSurfaceTexture = new SurfaceTexture(inputExternalTexId); + inputSurface = new Surface(inputSurfaceTexture); textureTransformMatrix = new float[16]; - framebuffers = new int[frameProcessors.size()]; outputSize = frameProcessors.isEmpty() ? externalCopyFrameProcessor.getOutputSize() @@ -185,26 +263,20 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } /** - * Configures the {@code FrameProcessorChain} to process frames to the specified output targets. + * Sets the output {@link Surface}. * - *

    This method may only be called once and may override the {@linkplain - * GlFrameProcessor#setInputSize(int, int) output size} of the final {@link GlFrameProcessor}. + *

    This method may override the output size of the final {@link GlFrameProcessor}. * * @param outputSurface The output {@link Surface}. * @param outputWidth The output width, in pixels. * @param outputHeight The output height, in pixels. * @param debugSurfaceView Optional debug {@link SurfaceView} to show output. - * @throws IllegalStateException If the {@code FrameProcessorChain} has already been configured. - * @throws TransformationException If reading shader files fails, or an OpenGL error occurs while - * creating and configuring the OpenGL components. */ - public void configure( + public void setOutputSurface( Surface outputSurface, int outputWidth, int outputHeight, - @Nullable SurfaceView debugSurfaceView) - throws TransformationException { - checkState(inputSurface == null, "The FrameProcessorChain has already been configured."); + @Nullable SurfaceView debugSurfaceView) { // TODO(b/218488308): Don't override output size for encoder fallback. Instead allow the final // GlFrameProcessor to be re-configured or append another GlFrameProcessor. outputSize = new Size(outputWidth, outputHeight); @@ -214,21 +286,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; debugPreviewHeight = debugSurfaceView.getHeight(); } - try { - // Wait for task to finish to be able to use inputExternalTexId to create the SurfaceTexture. - singleThreadExecutorService - .submit(this::createOpenGlObjectsAndInitializeFrameProcessors) - .get(); - } catch (ExecutionException e) { - throw TransformationException.createForFrameProcessorChain( - e, TransformationException.ERROR_CODE_GL_INIT_FAILED); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw TransformationException.createForFrameProcessorChain( - e, TransformationException.ERROR_CODE_GL_INIT_FAILED); - } + futures.add( + singleThreadExecutorService.submit( + () -> createOpenGlSurfaces(outputSurface, debugSurfaceView))); - inputSurfaceTexture = new SurfaceTexture(inputExternalTexId); inputSurfaceTexture.setOnFrameAvailableListener( surfaceTexture -> { if (releaseRequested) { @@ -244,21 +305,10 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } }); - inputSurface = new Surface(inputSurfaceTexture); - - futures.add( - singleThreadExecutorService.submit( - () -> createOpenGlSurfaces(outputSurface, debugSurfaceView))); } - /** - * Returns the input {@link Surface}. - * - *

    The {@code FrameProcessorChain} must be {@linkplain #configure(Surface, int, int, - * SurfaceView) configured}. - */ + /** Returns the input {@link Surface}. */ public Surface getInputSurface() { - checkStateNotNull(inputSurface, "The FrameProcessorChain must be configured."); return inputSurface; } @@ -347,9 +397,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** * Creates the OpenGL surfaces. * - *

    This method should only be called after {@link - * #createOpenGlObjectsAndInitializeFrameProcessors()} and must be called on the background - * thread. + *

    This method should only be called on the {@linkplain #THREAD_NAME background thread}. */ private void createOpenGlSurfaces(Surface outputSurface, @Nullable SurfaceView debugSurfaceView) { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); @@ -371,57 +419,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } } - /** - * Creates the OpenGL textures and framebuffers, and initializes the {@link GlFrameProcessor - * GlFrameProcessors}. - * - *

    This method should only be called on the background thread. - */ - private Void createOpenGlObjectsAndInitializeFrameProcessors() throws IOException { - checkState(Thread.currentThread().getName().equals(THREAD_NAME)); - - eglDisplay = GlUtil.createEglDisplay(); - eglContext = - enableExperimentalHdrEditing - ? GlUtil.createEglContextEs3Rgba1010102(eglDisplay) - : GlUtil.createEglContext(eglDisplay); - - if (GlUtil.isSurfacelessContextExtensionSupported()) { - GlUtil.focusEglSurface( - eglDisplay, eglContext, EGL14.EGL_NO_SURFACE, /* width= */ 1, /* height= */ 1); - } else if (enableExperimentalHdrEditing) { - // TODO(b/209404935): Don't assume BT.2020 PQ input/output. - GlUtil.focusPlaceholderEglSurfaceBt2020Pq(eglContext, eglDisplay); - } else { - GlUtil.focusPlaceholderEglSurface(eglContext, eglDisplay); - } - - inputExternalTexId = GlUtil.createExternalTexture(); - externalCopyFrameProcessor.initialize(inputExternalTexId); - - Size intermediateSize = externalCopyFrameProcessor.getOutputSize(); - for (int i = 0; i < frameProcessors.size(); i++) { - int inputTexId = - GlUtil.createTexture(intermediateSize.getWidth(), intermediateSize.getHeight()); - framebuffers[i] = GlUtil.createFboForTexture(inputTexId); - frameProcessors.get(i).initialize(inputTexId); - intermediateSize = frameProcessors.get(i).getOutputSize(); - } - // Return something because only Callables not Runnables can throw checked exceptions. - return null; - } - /** * Processes an input frame. * - *

    This method should only be called on the background thread. + *

    This method should only be called on the {@linkplain #THREAD_NAME background thread}. */ @RequiresNonNull("inputSurfaceTexture") private void processFrame() { checkState(Thread.currentThread().getName().equals(THREAD_NAME)); - checkStateNotNull(eglSurface); - checkStateNotNull(eglContext); - checkStateNotNull(eglDisplay); + checkStateNotNull(eglSurface, "No output surface set."); if (frameProcessors.isEmpty()) { GlUtil.focusEglSurface( diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java index 1f23f8bdd7..c0dc977c8e 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoTranscodingSamplePipeline.java @@ -116,7 +116,7 @@ import org.checkerframework.dataflow.qual.Pure; requestedEncoderFormat, encoderSupportedFormat)); - frameProcessorChain.configure( + frameProcessorChain.setOutputSurface( /* outputSurface= */ encoder.getInputSurface(), /* outputWidth= */ encoderSupportedFormat.width, /* outputHeight= */ encoderSupportedFormat.height, diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java index 0051978bdc..6fd88d043b 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/DefaultEncoderFactoryTest.java @@ -114,13 +114,19 @@ public class DefaultEncoderFactoryTest { @Test public void createForVideoEncoding_withNoSupportedEncoder_throws() { Format requestedVideoFormat = createVideoFormat(MimeTypes.VIDEO_H264, 1920, 1080, 30); - assertThrows( - TransformationException.class, - () -> - new DefaultEncoderFactory() - .createForVideoEncoding( - requestedVideoFormat, - /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H265))); + + TransformationException exception = + assertThrows( + TransformationException.class, + () -> + new DefaultEncoderFactory() + .createForVideoEncoding( + requestedVideoFormat, + /* allowedMimeTypes= */ ImmutableList.of(MimeTypes.VIDEO_H265))); + + assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); + assertThat(exception.errorCode) + .isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); } @Test diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java index ef47ce3433..510b74af10 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/TransformerEndToEndTest.java @@ -30,7 +30,6 @@ import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import android.content.Context; -import android.media.MediaCodecInfo; import android.media.MediaCrypto; import android.media.MediaFormat; import android.os.Handler; @@ -405,26 +404,6 @@ public final class TransformerEndToEndTest { .isEqualTo(TransformationException.ERROR_CODE_DECODING_FORMAT_UNSUPPORTED); } - @Test - public void startTransformation_withVideoEncoderFormatUnsupported_completesWithError() - throws Exception { - Transformer transformer = - createTransformerBuilder(/* enableFallback= */ false) - .setTransformationRequest( - new TransformationRequest.Builder() - .setVideoMimeType(MimeTypes.VIDEO_H263) // unsupported encoder MIME type - .build()) - .build(); - MediaItem mediaItem = MediaItem.fromUri(URI_PREFIX + FILE_VIDEO_ONLY); - - transformer.startTransformation(mediaItem, outputPath); - TransformationException exception = TransformerTestRunner.runUntilError(transformer); - - assertThat(exception).hasCauseThat().isInstanceOf(IllegalArgumentException.class); - assertThat(exception.errorCode) - .isEqualTo(TransformationException.ERROR_CODE_OUTPUT_FORMAT_UNSUPPORTED); - } - @Test public void startTransformation_withIoError_completesWithError() throws Exception { Transformer transformer = createTransformerBuilder(/* enableFallback= */ false).build(); @@ -801,11 +780,6 @@ public final class TransformerEndToEndTest { throwingCodecConfig, /* colorFormats= */ ImmutableList.of(), /* isDecoder= */ true); - addCodec( - MimeTypes.VIDEO_H263, - throwingCodecConfig, - ImmutableList.of(MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Flexible), - /* isDecoder= */ false); addCodec( MimeTypes.AUDIO_AMR_NB, throwingCodecConfig, From d3931d8b96eb67767b29433cc5ba0f2dca8c615e Mon Sep 17 00:00:00 2001 From: hschlueter Date: Fri, 1 Apr 2022 17:55:52 +0100 Subject: [PATCH 046/116] Add ExternalCopyFrameProcessor to frameProcessors list. PiperOrigin-RevId: 438847583 --- .../transformer/FrameProcessorChain.java | 54 +++++++------------ 1 file changed, 18 insertions(+), 36 deletions(-) diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 3199ed5b96..659ed2bf56 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -171,9 +171,11 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; eglContext, singleThreadExecutorService, inputExternalTexId, - externalCopyFrameProcessor, framebuffers, - ImmutableList.copyOf(frameProcessors), + new ImmutableList.Builder() + .add(externalCopyFrameProcessor) + .addAll(frameProcessors) + .build(), enableExperimentalHdrEditing); } @@ -196,14 +198,15 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; /** Transformation matrix associated with the {@link #inputSurfaceTexture}. */ private final float[] textureTransformMatrix; - private final ExternalCopyFrameProcessor externalCopyFrameProcessor; + /** + * Contains an {@link ExternalCopyFrameProcessor} at the 0th index and optionally other {@link + * GlFrameProcessor GlFrameProcessors} at indices >= 1. + */ private final ImmutableList frameProcessors; /** * Identifiers of a framebuffer object associated with the intermediate textures that receive * output from the previous {@link GlFrameProcessor}, and provide input for the following {@link * GlFrameProcessor}. - * - *

    The {@link ExternalCopyFrameProcessor} writes to the first framebuffer. */ private final int[] framebuffers; @@ -231,15 +234,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; EGLContext eglContext, ExecutorService singleThreadExecutorService, int inputExternalTexId, - ExternalCopyFrameProcessor externalCopyFrameProcessor, int[] framebuffers, ImmutableList frameProcessors, boolean enableExperimentalHdrEditing) { + checkState(!frameProcessors.isEmpty()); this.eglDisplay = eglDisplay; this.eglContext = eglContext; this.singleThreadExecutorService = singleThreadExecutorService; - this.externalCopyFrameProcessor = externalCopyFrameProcessor; this.framebuffers = framebuffers; this.frameProcessors = frameProcessors; this.enableExperimentalHdrEditing = enableExperimentalHdrEditing; @@ -249,10 +251,7 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; inputSurfaceTexture = new SurfaceTexture(inputExternalTexId); inputSurface = new Surface(inputSurfaceTexture); textureTransformMatrix = new float[16]; - outputSize = - frameProcessors.isEmpty() - ? externalCopyFrameProcessor.getOutputSize() - : getLast(frameProcessors).getOutputSize(); + outputSize = getLast(frameProcessors).getOutputSize(); debugPreviewWidth = C.LENGTH_UNSET; debugPreviewHeight = C.LENGTH_UNSET; } @@ -379,7 +378,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; futures.add( singleThreadExecutorService.submit( () -> { - externalCopyFrameProcessor.release(); for (int i = 0; i < frameProcessors.size(); i++) { frameProcessors.get(i).release(); } @@ -429,26 +427,12 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; checkState(Thread.currentThread().getName().equals(THREAD_NAME)); checkStateNotNull(eglSurface, "No output surface set."); - if (frameProcessors.isEmpty()) { - GlUtil.focusEglSurface( - eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); - } else { - Size intermediateSize = externalCopyFrameProcessor.getOutputSize(); - GlUtil.focusFramebuffer( - eglDisplay, - eglContext, - eglSurface, - framebuffers[0], - intermediateSize.getWidth(), - intermediateSize.getHeight()); - } inputSurfaceTexture.updateTexImage(); - inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); - externalCopyFrameProcessor.setTextureTransformMatrix(textureTransformMatrix); long presentationTimeNs = inputSurfaceTexture.getTimestamp(); long presentationTimeUs = presentationTimeNs / 1000; - clearOutputFrame(); - externalCopyFrameProcessor.updateProgramAndDraw(presentationTimeUs); + inputSurfaceTexture.getTransformMatrix(textureTransformMatrix); + ((ExternalCopyFrameProcessor) frameProcessors.get(0)) + .setTextureTransformMatrix(textureTransformMatrix); for (int i = 0; i < frameProcessors.size() - 1; i++) { Size intermediateSize = frameProcessors.get(i).getOutputSize(); @@ -456,18 +440,16 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; eglDisplay, eglContext, eglSurface, - framebuffers[i + 1], + framebuffers[i], intermediateSize.getWidth(), intermediateSize.getHeight()); clearOutputFrame(); frameProcessors.get(i).updateProgramAndDraw(presentationTimeUs); } - if (!frameProcessors.isEmpty()) { - GlUtil.focusEglSurface( - eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); - clearOutputFrame(); - getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); - } + GlUtil.focusEglSurface( + eglDisplay, eglContext, eglSurface, outputSize.getWidth(), outputSize.getHeight()); + clearOutputFrame(); + getLast(frameProcessors).updateProgramAndDraw(presentationTimeUs); EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, presentationTimeNs); EGL14.eglSwapBuffers(eglDisplay, eglSurface); From de00030f98dd9ad743bc4c99413a97877b653cec Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 4 Apr 2022 10:56:22 +0100 Subject: [PATCH 047/116] Merge GlFrameProcessor#setInputSize() and initialize(). PiperOrigin-RevId: 439266087 --- .../AdvancedFrameProcessorPixelTest.java | 8 +-- .../transformer/FrameProcessorChainTest.java | 5 +- .../transformer/AdvancedFrameProcessor.java | 16 ++--- .../ExternalCopyFrameProcessor.java | 16 ++--- .../transformer/FrameProcessorChain.java | 6 +- .../media3/transformer/GlFrameProcessor.java | 35 ++++------ .../PresentationFrameProcessor.java | 70 +++++++++---------- .../transformer/ScaleToFitFrameProcessor.java | 62 ++++++++-------- .../AdvancedFrameProcessorTest.java | 66 ----------------- .../PresentationFrameProcessorTest.java | 8 +-- .../ScaleToFitFrameProcessorTest.java | 24 ++----- 11 files changed, 102 insertions(+), 214 deletions(-) delete mode 100644 libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java index 9fba1d9482..724eec72b8 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/AdvancedFrameProcessorPixelTest.java @@ -91,7 +91,7 @@ public final class AdvancedFrameProcessorPixelTest { String testId = "updateProgramAndDraw_noEdits"; Matrix identityMatrix = new Matrix(); advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), identityMatrix); - advancedFrameProcessor.initialize(inputTexId); + advancedFrameProcessor.initialize(inputTexId, width, height); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(FIRST_FRAME_PNG_ASSET_STRING); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); @@ -114,7 +114,7 @@ public final class AdvancedFrameProcessorPixelTest { translateRightMatrix.postTranslate(/* dx= */ 1, /* dy= */ 0); advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), translateRightMatrix); - advancedFrameProcessor.initialize(inputTexId); + advancedFrameProcessor.initialize(inputTexId, width, height); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(TRANSLATE_RIGHT_EXPECTED_OUTPUT_PNG_ASSET_STRING); @@ -137,7 +137,7 @@ public final class AdvancedFrameProcessorPixelTest { Matrix scaleNarrowMatrix = new Matrix(); scaleNarrowMatrix.postScale(.5f, 1.2f); advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), scaleNarrowMatrix); - advancedFrameProcessor.initialize(inputTexId); + advancedFrameProcessor.initialize(inputTexId, width, height); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(SCALE_NARROW_EXPECTED_OUTPUT_PNG_ASSET_STRING); @@ -160,7 +160,7 @@ public final class AdvancedFrameProcessorPixelTest { Matrix rotate90Matrix = new Matrix(); rotate90Matrix.postRotate(/* degrees= */ 90); advancedFrameProcessor = new AdvancedFrameProcessor(getApplicationContext(), rotate90Matrix); - advancedFrameProcessor.initialize(inputTexId); + advancedFrameProcessor.initialize(inputTexId, width, height); Bitmap expectedBitmap = BitmapTestUtil.readBitmap(ROTATE_90_EXPECTED_OUTPUT_PNG_ASSET_STRING); advancedFrameProcessor.updateProgramAndDraw(/* presentationTimeUs= */ 0); diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java index 051eb3aefe..ee81429b44 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/FrameProcessorChainTest.java @@ -138,16 +138,13 @@ public final class FrameProcessorChainTest { } @Override - public void setInputSize(int inputWidth, int inputHeight) {} + public void initialize(int inputTexId, int inputWidth, int inputHeight) {} @Override public Size getOutputSize() { return outputSize; } - @Override - public void initialize(int inputTexId) {} - @Override public void updateProgramAndDraw(long presentationTimeNs) {} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index 6996c0111c..cc5b23ec57 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -106,17 +106,8 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { } @Override - public void setInputSize(int inputWidth, int inputHeight) { + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { size = new Size(inputWidth, inputHeight); - } - - @Override - public Size getOutputSize() { - return checkStateNotNull(size); - } - - @Override - public void initialize(int inputTexId) throws IOException { // TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms // expected in the code. glProgram = new GlProgram(context, VERTEX_SHADER_TRANSFORMATION_PATH, FRAGMENT_SHADER_PATH); @@ -129,6 +120,11 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { glProgram.setFloatsUniform("uTransformationMatrix", getGlMatrixArray(transformationMatrix)); } + @Override + public Size getOutputSize() { + return checkStateNotNull(size); + } + @Override public void updateProgramAndDraw(long presentationTimeUs) { checkStateNotNull(glProgram); diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java index 8fa78ec3fc..eaeeedad05 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ExternalCopyFrameProcessor.java @@ -60,17 +60,8 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } @Override - public void setInputSize(int inputWidth, int inputHeight) { + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { size = new Size(inputWidth, inputHeight); - } - - @Override - public Size getOutputSize() { - return checkStateNotNull(size); - } - - @Override - public void initialize(int inputTexId) throws IOException { // TODO(b/205002913): check the loaded program is consistent with the attributes and uniforms // expected in the code. String vertexShaderFilePath = @@ -94,6 +85,11 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; } } + @Override + public Size getOutputSize() { + return checkStateNotNull(size); + } + /** * Sets the texture transform matrix for converting an external surface texture's coordinates to * sampling locations. diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java index 659ed2bf56..b5dfe5af2a 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/FrameProcessorChain.java @@ -154,16 +154,14 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; } int inputExternalTexId = GlUtil.createExternalTexture(); - externalCopyFrameProcessor.setInputSize(inputWidth, inputHeight); - externalCopyFrameProcessor.initialize(inputExternalTexId); + externalCopyFrameProcessor.initialize(inputExternalTexId, inputWidth, inputHeight); int[] framebuffers = new int[frameProcessors.size()]; Size inputSize = externalCopyFrameProcessor.getOutputSize(); for (int i = 0; i < frameProcessors.size(); i++) { int inputTexId = GlUtil.createTexture(inputSize.getWidth(), inputSize.getHeight()); framebuffers[i] = GlUtil.createFboForTexture(inputTexId); - frameProcessors.get(i).setInputSize(inputSize.getWidth(), inputSize.getHeight()); - frameProcessors.get(i).initialize(inputTexId); + frameProcessors.get(i).initialize(inputTexId, inputSize.getWidth(), inputSize.getHeight()); inputSize = frameProcessors.get(i).getOutputSize(); } return new FrameProcessorChain( diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java index c437df536c..31ff97ad14 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/GlFrameProcessor.java @@ -26,50 +26,41 @@ import java.io.IOException; * *

      *
    1. The constructor, for implementation-specific arguments. - *
    2. {@link #setInputSize(int, int)}, to configure based on input dimensions. - *
    3. {@link #initialize(int)}, to set up graphics initialization. + *
    4. {@link #initialize(int,int,int)}, to set up graphics initialization. *
    5. {@link #updateProgramAndDraw(long)}, to process one frame. *
    6. {@link #release()}, upon conclusion of processing. *
    */ @UnstableApi public interface GlFrameProcessor { - // TODO(b/213313666): Investigate whether all configuration can be moved to initialize by - // using a placeholder surface until the encoder surface is known. If so, convert - // configureOutputSize to a simple getter. /** - * Sets the input size of frames processed through {@link #updateProgramAndDraw(long)}. + * Performs all initialization that requires OpenGL, such as, loading and compiling a GLSL shader + * program. * - *

    This method must be called before {@link #initialize(int)} and does not use OpenGL, as - * calling this method without a current OpenGL context is allowed. + *

    This method may only be called if there is a current OpenGL context. * - *

    After setting the input size, the output size can be obtained using {@link - * #getOutputSize()}. + * @param inputTexId Identifier of a 2D OpenGL texture. + * @param inputWidth The input width, in pixels. + * @param inputHeight The input height, in pixels. */ - void setInputSize(int inputWidth, int inputHeight); + void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException; /** * Returns the output {@link Size} of frames processed through {@link * #updateProgramAndDraw(long)}. * - *

    Must call {@link #setInputSize(int, int)} before calling this method. + *

    This method may only be called after the frame processor has been {@link + * #initialize(int,int,int) initialized}. */ Size getOutputSize(); - /** - * Does any initialization necessary such as loading and compiling a GLSL shader programs. - * - *

    This method may only be called after creating the OpenGL context and focusing a render - * target. - */ - void initialize(int inputTexId) throws IOException; - /** * Updates the shader program's vertex attributes and uniforms, binds them, and draws. * - *

    The frame processor must be {@linkplain #initialize(int) initialized}. The caller is - * responsible for focussing the correct render target before calling this method. + *

    This method may only be called after the frame processor has been {@link + * #initialize(int,int,int) initialized}. The caller is responsible for focussing the correct + * render target before calling this method. * * @param presentationTimeUs The presentation timestamp of the current frame, in microseconds. */ diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java index 09103b3a9e..8df3d0f9ec 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/PresentationFrameProcessor.java @@ -21,11 +21,13 @@ import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.content.Context; import android.graphics.Matrix; import android.util.Size; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.UnstableApi; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** Controls how a frame is viewed, by changing resolution. */ @@ -35,6 +37,7 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { /** A builder for {@link PresentationFrameProcessor} instances. */ public static final class Builder { + // Mandatory field. private final Context context; @@ -80,12 +83,10 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { private final Context context; private final int requestedHeight; - private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; - private int inputWidth; - private int inputHeight; - private int outputRotationDegrees; private @MonotonicNonNull Size outputSize; + private int outputRotationDegrees; private @MonotonicNonNull Matrix transformationMatrix; + private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; /** * Creates a new instance. @@ -97,17 +98,27 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { this.context = context; this.requestedHeight = requestedHeight; - inputWidth = C.LENGTH_UNSET; - inputHeight = C.LENGTH_UNSET; outputRotationDegrees = C.LENGTH_UNSET; } + @Override + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + advancedFrameProcessor = new AdvancedFrameProcessor(context, transformationMatrix); + advancedFrameProcessor.initialize(inputTexId, inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + /** * Returns {@link Format#rotationDegrees} for the output frame. * *

    Return values may be {@code 0} or {@code 90} degrees. * - *

    This method can only be called after {@link #setInputSize(int, int)}. + *

    The frame processor must be {@linkplain #initialize(int,int,int) initialized}. */ public int getOutputRotationDegrees() { checkState(outputRotationDegrees != C.LENGTH_UNSET); @@ -115,20 +126,28 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { } @Override - public void setInputSize(int inputWidth, int inputHeight) { - this.inputWidth = inputWidth; - this.inputHeight = inputHeight; - transformationMatrix = new Matrix(); + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); + } + @Override + public void release() { + if (advancedFrameProcessor != null) { + advancedFrameProcessor.release(); + } + } + + @EnsuresNonNull("transformationMatrix") + @VisibleForTesting // Allows roboletric testing of output size calculation without OpenGL. + /* package */ void configureOutputSizeAndTransformationMatrix(int inputWidth, int inputHeight) { + transformationMatrix = new Matrix(); int displayWidth = inputWidth; int displayHeight = inputHeight; - // Scale width and height to desired requestedHeight, preserving aspect ratio. if (requestedHeight != C.LENGTH_UNSET && requestedHeight != displayHeight) { displayWidth = Math.round((float) requestedHeight * displayWidth / displayHeight); displayHeight = requestedHeight; } - // Encoders commonly support higher maximum widths than maximum heights. Rotate the decoded // frame before encoding, so the encoded frame's width >= height, and set // outputRotationDegrees to ensure the frame is displayed in the correct orientation. @@ -143,29 +162,4 @@ public final class PresentationFrameProcessor implements GlFrameProcessor { outputSize = new Size(displayWidth, displayHeight); } } - - @Override - public Size getOutputSize() { - return checkStateNotNull(outputSize); - } - - @Override - public void initialize(int inputTexId) throws IOException { - checkStateNotNull(transformationMatrix); - advancedFrameProcessor = new AdvancedFrameProcessor(context, transformationMatrix); - advancedFrameProcessor.setInputSize(inputWidth, inputHeight); - advancedFrameProcessor.initialize(inputTexId); - } - - @Override - public void updateProgramAndDraw(long presentationTimeUs) { - checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); - } - - @Override - public void release() { - if (advancedFrameProcessor != null) { - advancedFrameProcessor.release(); - } - } } diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java index 5ca8730a2b..74032637e3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/ScaleToFitFrameProcessor.java @@ -22,10 +22,11 @@ import static java.lang.Math.min; import android.content.Context; import android.graphics.Matrix; import android.util.Size; -import androidx.media3.common.C; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.util.GlUtil; import androidx.media3.common.util.UnstableApi; import java.io.IOException; +import org.checkerframework.checker.nullness.qual.EnsuresNonNull; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -100,8 +101,6 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { private final Matrix transformationMatrix; private @MonotonicNonNull AdvancedFrameProcessor advancedFrameProcessor; - private int inputWidth; - private int inputHeight; private @MonotonicNonNull Size outputSize; private @MonotonicNonNull Matrix adjustedTransformationMatrix; @@ -120,15 +119,35 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { this.transformationMatrix = new Matrix(); this.transformationMatrix.postScale(scaleX, scaleY); this.transformationMatrix.postRotate(rotationDegrees); - - inputWidth = C.LENGTH_UNSET; - inputHeight = C.LENGTH_UNSET; } @Override - public void setInputSize(int inputWidth, int inputHeight) { - this.inputWidth = inputWidth; - this.inputHeight = inputHeight; + public void initialize(int inputTexId, int inputWidth, int inputHeight) throws IOException { + configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); + advancedFrameProcessor = new AdvancedFrameProcessor(context, adjustedTransformationMatrix); + advancedFrameProcessor.initialize(inputTexId, inputWidth, inputHeight); + } + + @Override + public Size getOutputSize() { + return checkStateNotNull(outputSize); + } + + @Override + public void updateProgramAndDraw(long presentationTimeUs) { + checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); + } + + @Override + public void release() { + if (advancedFrameProcessor != null) { + advancedFrameProcessor.release(); + } + } + + @EnsuresNonNull("adjustedTransformationMatrix") + @VisibleForTesting // Allows roboletric testing of output size calculation without OpenGL. + /* package */ void configureOutputSizeAndTransformationMatrix(int inputWidth, int inputHeight) { adjustedTransformationMatrix = new Matrix(transformationMatrix); if (transformationMatrix.isIdentity()) { @@ -165,29 +184,4 @@ public final class ScaleToFitFrameProcessor implements GlFrameProcessor { adjustedTransformationMatrix.postScale(1f / xScale, 1f / yScale); outputSize = new Size(Math.round(inputWidth * xScale), Math.round(inputHeight * yScale)); } - - @Override - public Size getOutputSize() { - return checkStateNotNull(outputSize); - } - - @Override - public void initialize(int inputTexId) throws IOException { - checkStateNotNull(adjustedTransformationMatrix); - advancedFrameProcessor = new AdvancedFrameProcessor(context, adjustedTransformationMatrix); - advancedFrameProcessor.setInputSize(inputWidth, inputHeight); - advancedFrameProcessor.initialize(inputTexId); - } - - @Override - public void updateProgramAndDraw(long presentationTimeUs) { - checkStateNotNull(advancedFrameProcessor).updateProgramAndDraw(presentationTimeUs); - } - - @Override - public void release() { - if (advancedFrameProcessor != null) { - advancedFrameProcessor.release(); - } - } } diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java deleted file mode 100644 index 7ef38b86d5..0000000000 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2022 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package androidx.media3.transformer; - -import static androidx.test.core.app.ApplicationProvider.getApplicationContext; -import static com.google.common.truth.Truth.assertThat; - -import android.graphics.Matrix; -import android.util.Size; -import androidx.test.ext.junit.runners.AndroidJUnit4; -import org.junit.Test; -import org.junit.runner.RunWith; - -/** - * Unit tests for {@link AdvancedFrameProcessor}. - * - *

    See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link - * AdvancedFrameProcessor} given a transformation matrix. - */ -@RunWith(AndroidJUnit4.class) -public final class AdvancedFrameProcessorTest { - @Test - public void getOutputSize_withIdentityMatrix_leavesSizeUnchanged() { - Matrix identityMatrix = new Matrix(); - int inputWidth = 200; - int inputHeight = 150; - AdvancedFrameProcessor advancedFrameProcessor = - new AdvancedFrameProcessor(getApplicationContext(), identityMatrix); - - advancedFrameProcessor.setInputSize(inputWidth, inputHeight); - Size outputSize = advancedFrameProcessor.getOutputSize(); - - assertThat(outputSize.getWidth()).isEqualTo(inputWidth); - assertThat(outputSize.getHeight()).isEqualTo(inputHeight); - } - - @Test - public void getOutputSize_withTransformationMatrix_leavesSizeUnchanged() { - Matrix transformationMatrix = new Matrix(); - transformationMatrix.postRotate(/* degrees= */ 90); - transformationMatrix.postScale(/* sx= */ .5f, /* sy= */ 1.2f); - int inputWidth = 200; - int inputHeight = 150; - AdvancedFrameProcessor advancedFrameProcessor = - new AdvancedFrameProcessor(getApplicationContext(), transformationMatrix); - - advancedFrameProcessor.setInputSize(inputWidth, inputHeight); - Size outputSize = advancedFrameProcessor.getOutputSize(); - - assertThat(outputSize.getWidth()).isEqualTo(inputWidth); - assertThat(outputSize.getHeight()).isEqualTo(inputHeight); - } -} diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java index 3f3047c01c..8157208f62 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/PresentationFrameProcessorTest.java @@ -39,7 +39,7 @@ public final class PresentationFrameProcessorTest { PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); @@ -54,7 +54,7 @@ public final class PresentationFrameProcessorTest { PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); @@ -69,7 +69,7 @@ public final class PresentationFrameProcessorTest { PresentationFrameProcessor presentationFrameProcessor = new PresentationFrameProcessor.Builder(getApplicationContext()).build(); - presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(90); @@ -87,7 +87,7 @@ public final class PresentationFrameProcessorTest { .setResolution(requestedHeight) .build(); - presentationFrameProcessor.setInputSize(inputWidth, inputHeight); + presentationFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = presentationFrameProcessor.getOutputSize(); assertThat(presentationFrameProcessor.getOutputRotationDegrees()).isEqualTo(0); diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java index 5f4683b4a5..989a73a1b7 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/ScaleToFitFrameProcessorTest.java @@ -17,7 +17,6 @@ package androidx.media3.transformer; import static androidx.test.core.app.ApplicationProvider.getApplicationContext; import static com.google.common.truth.Truth.assertThat; -import static org.junit.Assert.assertThrows; import android.util.Size; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -40,24 +39,13 @@ public final class ScaleToFitFrameProcessorTest { ScaleToFitFrameProcessor scaleToFitFrameProcessor = new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build(); - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); assertThat(outputSize.getHeight()).isEqualTo(inputHeight); } - @Test - public void initializeBeforeConfigure_throwsIllegalStateException() { - ScaleToFitFrameProcessor scaleToFitFrameProcessor = - new ScaleToFitFrameProcessor.Builder(getApplicationContext()).build(); - - // configureOutputSize not called before initialize. - assertThrows( - IllegalStateException.class, - () -> scaleToFitFrameProcessor.initialize(/* inputTexId= */ 0)); - } - @Test public void getOutputSize_scaleNarrow_decreasesWidth() { int inputWidth = 200; @@ -67,7 +55,7 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ .5f, /* scaleY= */ 1f) .build(); - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(Math.round(inputWidth * .5f)); @@ -83,7 +71,7 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ 2f, /* scaleY= */ 1f) .build(); - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth * 2); @@ -99,7 +87,7 @@ public final class ScaleToFitFrameProcessorTest { .setScale(/* scaleX= */ 1f, /* scaleY= */ 2f) .build(); - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputWidth); @@ -115,7 +103,7 @@ public final class ScaleToFitFrameProcessorTest { .setRotationDegrees(90) .build(); - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(inputHeight); @@ -132,7 +120,7 @@ public final class ScaleToFitFrameProcessorTest { .build(); long expectedOutputWidthHeight = 247; - scaleToFitFrameProcessor.setInputSize(inputWidth, inputHeight); + scaleToFitFrameProcessor.configureOutputSizeAndTransformationMatrix(inputWidth, inputHeight); Size outputSize = scaleToFitFrameProcessor.getOutputSize(); assertThat(outputSize.getWidth()).isEqualTo(expectedOutputWidthHeight); From f6808c9645eebcb36ede3cc5916f1c919a7b710b Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Mon, 4 Apr 2022 11:07:29 +0100 Subject: [PATCH 048/116] Transformer Demo: Open source previously-internal demo video files. PiperOrigin-RevId: 439267827 --- .../media3/demo/transformer/ConfigurationActivity.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index bc0219f6f5..7f87af30fc 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -63,6 +63,10 @@ public final class ConfigurationActivity extends AppCompatActivity { "https://html5demos.com/assets/dizzy.webm", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_4k60.mp4", "https://storage.googleapis.com/exoplayer-test-media-1/mp4/8k24fps_4s.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/portrait_rotated_avc_aac.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/slow-motion/slowMotion_stopwatch_240fps_long.mp4", + "https://storage.googleapis.com/exoplayer-test-media-1/mp4/samsung-hdr-hdr10.mp4", }; private static final String[] URI_DESCRIPTIONS = { // same order as INPUT_URIS "MP4 with H264 video and AAC audio", @@ -71,6 +75,10 @@ public final class ConfigurationActivity extends AppCompatActivity { "WebM with VP8 video and Vorbis audio", "4K 60fps MP4 with H264 video and AAC audio (portrait, timestamps always increase)", "8k 24fps MP4 with H265 video and AAC audio", + "MP4 with H264 video and AAC audio (portrait, H > W, 0\u00B0)", + "MP4 with H264 video and AAC audio (portrait, H < W, 90\u00B0)", + "SEF slow motion with 240 fps", + "MP4 with HDR (HDR10) H265 video (encoding may fail)", }; private static final String SAME_AS_INPUT_OPTION = "same as input"; From 5b77625582fac4146d32f13ac3f3de3a5de0155c Mon Sep 17 00:00:00 2001 From: claincly Date: Mon, 4 Apr 2022 11:10:32 +0100 Subject: [PATCH 049/116] Add test to evaluate performance related encoding parameters. PiperOrigin-RevId: 439268235 --- .../EncoderPerformanceAnalysisTest.java | 122 ++++++++++++++++++ .../transformer/DefaultEncoderFactory.java | 11 ++ .../transformer/VideoEncoderSettings.java | 47 ++++++- 3 files changed, 177 insertions(+), 3 deletions(-) create mode 100644 libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java new file mode 100644 index 0000000000..9a289ab944 --- /dev/null +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/mh/analysis/EncoderPerformanceAnalysisTest.java @@ -0,0 +1,122 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package androidx.media3.transformer.mh.analysis; + +import static androidx.media3.common.util.Assertions.checkNotNull; + +import android.content.Context; +import android.media.MediaFormat; +import android.net.Uri; +import androidx.media3.common.util.Util; +import androidx.media3.transformer.AndroidTestUtil; +import androidx.media3.transformer.DefaultEncoderFactory; +import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.Transformer; +import androidx.media3.transformer.TransformerAndroidTestRunner; +import androidx.media3.transformer.VideoEncoderSettings; +import androidx.test.core.app.ApplicationProvider; +import com.google.common.collect.ImmutableList; +import java.util.HashMap; +import java.util.Map; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +/** Instrumentation tests for analyzing encoder performance settings. */ +@RunWith(Parameterized.class) +public class EncoderPerformanceAnalysisTest { + + /** A non-realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ + private static final int MEDIA_CODEC_PRIORITY_NON_REALTIME = 0; + /** A realtime {@link MediaFormat#KEY_PRIORITY encoder priority}. */ + private static final int MEDIA_CODEC_PRIORITY_REALTIME = 1; + + private static final ImmutableList INPUT_FILES = + ImmutableList.of( + AndroidTestUtil.MP4_ASSET_WITH_INCREASING_TIMESTAMPS_URI_STRING, + AndroidTestUtil.MP4_REMOTE_4K60_PORTRAIT_URI_STRING); + + private static final ImmutableList OPERATING_RATE_SETTINGS = + ImmutableList.of(VideoEncoderSettings.NO_VALUE, 30, Integer.MAX_VALUE); + + private static final ImmutableList PRIORITY_SETTINGS = + ImmutableList.of( + // Use NO_VALUE to skip setting priority. + VideoEncoderSettings.NO_VALUE, + MEDIA_CODEC_PRIORITY_NON_REALTIME, + MEDIA_CODEC_PRIORITY_REALTIME); + + @Parameter(0) + public @MonotonicNonNull String fileUri; + + @Parameter(1) + public int operatingRate; + + @Parameter(2) + public int priority; + + @Parameters(name = "analyzePerformance_{0}_OpRate={1}_Priority={2}") + public static ImmutableList parameters() { + ImmutableList.Builder parametersBuilder = new ImmutableList.Builder<>(); + for (int i = 0; i < INPUT_FILES.size(); i++) { + for (int j = 0; j < OPERATING_RATE_SETTINGS.size(); j++) { + for (int k = 0; k < PRIORITY_SETTINGS.size(); k++) { + parametersBuilder.add( + new Object[] { + INPUT_FILES.get(i), OPERATING_RATE_SETTINGS.get(j), PRIORITY_SETTINGS.get(k) + }); + } + } + } + return parametersBuilder.build(); + } + + @Test + public void analyzeEncoderPerformance() throws Exception { + checkNotNull(fileUri); + String filename = checkNotNull(Uri.parse(fileUri).getLastPathSegment()); + String testId = + Util.formatInvariant( + "analyzePerformance_%s_OpRate_%d_Priority_%d", filename, operatingRate, priority); + + Map inputValues = new HashMap<>(); + inputValues.put("inputFilename", filename); + inputValues.put("operatingRate", operatingRate); + inputValues.put("priority", priority); + + Context context = ApplicationProvider.getApplicationContext(); + Transformer transformer = + new Transformer.Builder(context) + .setRemoveAudio(true) + .setEncoderFactory( + new DefaultEncoderFactory( + EncoderSelector.DEFAULT, + new VideoEncoderSettings.Builder() + .setEncoderPerformanceParameters(operatingRate, priority) + .build(), + /* enableFallback= */ false)) + .build(); + + new TransformerAndroidTestRunner.Builder(context, transformer) + .setInputValues(inputValues) + .build() + .run(testId, fileUri); + } +} diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java index 70398ced73..ef3c612a30 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/DefaultEncoderFactory.java @@ -185,6 +185,17 @@ public final class DefaultEncoderFactory implements Codec.EncoderFactory { mediaFormat.setFloat( MediaFormat.KEY_I_FRAME_INTERVAL, supportedVideoEncoderSettings.iFrameIntervalSeconds); + if (Util.SDK_INT >= 23) { + // Setting operating rate and priority is supported from API 23. + if (supportedVideoEncoderSettings.operatingRate != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger( + MediaFormat.KEY_OPERATING_RATE, supportedVideoEncoderSettings.operatingRate); + } + if (supportedVideoEncoderSettings.priority != VideoEncoderSettings.NO_VALUE) { + mediaFormat.setInteger(MediaFormat.KEY_PRIORITY, supportedVideoEncoderSettings.priority); + } + } + return new DefaultCodec( format, mediaFormat, diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java index 257795a52c..90db319e17 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/VideoEncoderSettings.java @@ -21,8 +21,10 @@ import static java.lang.annotation.ElementType.TYPE_USE; import android.annotation.SuppressLint; import android.media.MediaCodecInfo; +import android.media.MediaFormat; import androidx.annotation.IntDef; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import androidx.media3.common.Format; import androidx.media3.common.util.UnstableApi; import java.lang.annotation.Documented; @@ -76,6 +78,8 @@ public final class VideoEncoderSettings { private int level; private int colorProfile; private float iFrameIntervalSeconds; + private int operatingRate; + private int priority; /** Creates a new instance. */ public Builder() { @@ -85,6 +89,8 @@ public final class VideoEncoderSettings { this.level = NO_VALUE; this.colorProfile = DEFAULT_COLOR_PROFILE; this.iFrameIntervalSeconds = DEFAULT_I_FRAME_INTERVAL_SECONDS; + this.operatingRate = NO_VALUE; + this.priority = NO_VALUE; } private Builder(VideoEncoderSettings videoEncoderSettings) { @@ -94,6 +100,8 @@ public final class VideoEncoderSettings { this.level = videoEncoderSettings.level; this.colorProfile = videoEncoderSettings.colorProfile; this.iFrameIntervalSeconds = videoEncoderSettings.iFrameIntervalSeconds; + this.operatingRate = videoEncoderSettings.operatingRate; + this.priority = videoEncoderSettings.priority; } /** @@ -172,10 +180,31 @@ public final class VideoEncoderSettings { return this; } + /** + * Sets encoding operating rate and priority. The default values are {@link #NO_VALUE}. + * + * @param operatingRate The {@link MediaFormat#KEY_OPERATING_RATE operating rate}. + * @param priority The {@link MediaFormat#KEY_PRIORITY priority}. + * @return This builder. + */ + @VisibleForTesting + public Builder setEncoderPerformanceParameters(int operatingRate, int priority) { + this.operatingRate = operatingRate; + this.priority = priority; + return this; + } + /** Builds the instance. */ public VideoEncoderSettings build() { return new VideoEncoderSettings( - bitrate, bitrateMode, profile, level, colorProfile, iFrameIntervalSeconds); + bitrate, + bitrateMode, + profile, + level, + colorProfile, + iFrameIntervalSeconds, + operatingRate, + priority); } } @@ -191,6 +220,10 @@ public final class VideoEncoderSettings { public final int colorProfile; /** The encoding I-Frame interval in seconds. */ public final float iFrameIntervalSeconds; + /** The encoder {@link MediaFormat#KEY_OPERATING_RATE operating rate}. */ + public final int operatingRate; + /** The encoder {@link MediaFormat#KEY_PRIORITY priority}. */ + public final int priority; private VideoEncoderSettings( int bitrate, @@ -198,13 +231,17 @@ public final class VideoEncoderSettings { int profile, int level, int colorProfile, - float iFrameIntervalSeconds) { + float iFrameIntervalSeconds, + int operatingRate, + int priority) { this.bitrate = bitrate; this.bitrateMode = bitrateMode; this.profile = profile; this.level = level; this.colorProfile = colorProfile; this.iFrameIntervalSeconds = iFrameIntervalSeconds; + this.operatingRate = operatingRate; + this.priority = priority; } /** @@ -228,7 +265,9 @@ public final class VideoEncoderSettings { && profile == that.profile && level == that.level && colorProfile == that.colorProfile - && iFrameIntervalSeconds == that.iFrameIntervalSeconds; + && iFrameIntervalSeconds == that.iFrameIntervalSeconds + && operatingRate == that.operatingRate + && priority == that.priority; } @Override @@ -240,6 +279,8 @@ public final class VideoEncoderSettings { result = 31 * result + level; result = 31 * result + colorProfile; result = 31 * result + Float.floatToIntBits(iFrameIntervalSeconds); + result = 31 * result + operatingRate; + result = 31 * result + priority; return result; } } From 8138a9f48f4751dafc3aaa877a808627104f4fba Mon Sep 17 00:00:00 2001 From: hschlueter Date: Mon, 4 Apr 2022 15:42:19 +0100 Subject: [PATCH 050/116] Support android.opengl.Matrix in AdvancedFrameProcessor. This allows apps to use AdvancedFrameProcessor to apply transformations in 3D space. This functionality is not used in transformer otherwise. PiperOrigin-RevId: 439313406 --- .../transformer/AdvancedFrameProcessor.java | 30 ++++++++++--- .../AdvancedFrameProcessorTest.java | 45 +++++++++++++++++++ 2 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java diff --git a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java index cc5b23ec57..583be441e3 100644 --- a/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java +++ b/libraries/transformer/src/main/java/androidx/media3/transformer/AdvancedFrameProcessor.java @@ -15,6 +15,7 @@ */ package androidx.media3.transformer; +import static androidx.media3.common.util.Assertions.checkArgument; import static androidx.media3.common.util.Assertions.checkStateNotNull; import android.content.Context; @@ -46,7 +47,8 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { private static final String FRAGMENT_SHADER_PATH = "shaders/fragment_shader_copy_es2.glsl"; /** - * Returns a 4x4, column-major Matrix float array, from an input {@link Matrix}. + * Returns a 4x4, column-major {@link android.opengl.Matrix} float array, from an input {@link + * Matrix}. * *

    This is useful for converting to the 4x4 column-major format commonly used in OpenGL. */ @@ -87,7 +89,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { } private final Context context; - private final Matrix transformationMatrix; + private final float[] transformationMatrix; private @MonotonicNonNull Size size; private @MonotonicNonNull GlProgram glProgram; @@ -96,13 +98,27 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { * Creates a new instance. * * @param context The {@link Context}. - * @param transformationMatrix The transformation matrix to apply to each frame. Operations are - * done on normalized device coordinates (-1 to 1 on x and y), and no automatic adjustments - * are applied on the transformation matrix. + * @param transformationMatrix The transformation {@link Matrix} to apply to each frame. + * Operations are done on normalized device coordinates (-1 to 1 on x and y), and no automatic + * adjustments are applied on the transformation matrix. */ public AdvancedFrameProcessor(Context context, Matrix transformationMatrix) { + this(context, getGlMatrixArray(transformationMatrix)); + } + + /** + * Creates a new instance. + * + * @param context The {@link Context}. + * @param transformationMatrix The 4x4 transformation {@link android.opengl.Matrix} to apply to + * each frame. Operations are done on normalized device coordinates (-1 to 1 on x and y), and + * no automatic adjustments are applied on the transformation matrix. + */ + public AdvancedFrameProcessor(Context context, float[] transformationMatrix) { + checkArgument( + transformationMatrix.length == 16, "A 4x4 transformation matrix must have 16 elements."); this.context = context; - this.transformationMatrix = new Matrix(transformationMatrix); + this.transformationMatrix = transformationMatrix.clone(); } @Override @@ -117,7 +133,7 @@ public final class AdvancedFrameProcessor implements GlFrameProcessor { "aFramePosition", GlUtil.getNormalizedCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); glProgram.setBufferAttribute( "aTexSamplingCoord", GlUtil.getTextureCoordinateBounds(), GlUtil.RECTANGLE_VERTICES_COUNT); - glProgram.setFloatsUniform("uTransformationMatrix", getGlMatrixArray(transformationMatrix)); + glProgram.setFloatsUniform("uTransformationMatrix", transformationMatrix); } @Override diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java new file mode 100644 index 0000000000..bd4e16d567 --- /dev/null +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java @@ -0,0 +1,45 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.transformer; + +import static androidx.test.core.app.ApplicationProvider.getApplicationContext; +import static org.junit.Assert.assertThrows; + +import androidx.test.ext.junit.runners.AndroidJUnit4; +import org.junit.Test; +import org.junit.runner.RunWith; + +/** + * Unit tests for {@link AdvancedFrameProcessor}. + * + *

    See {@code AdvancedFrameProcessorPixelTest} for pixel tests testing {@link + * AdvancedFrameProcessor} given a transformation matrix. + */ +@RunWith(AndroidJUnit4.class) +public final class AdvancedFrameProcessorTest { + + @Test + public void construct_withInvalidMatrixSize_throwsException() { + assertThrows( + IllegalArgumentException.class, + () -> new AdvancedFrameProcessor(getApplicationContext(), new float[4])); + } + + @Test + public void construct_withValidMatrixSize_completesSucessfully() { + new AdvancedFrameProcessor(getApplicationContext(), new float[16]); + } +} From 5b258ef8ab18180ef428335cff4833aa7d7cea15 Mon Sep 17 00:00:00 2001 From: claincly Date: Mon, 4 Apr 2022 17:16:11 +0100 Subject: [PATCH 051/116] Add javadoc for SSIM helper. PiperOrigin-RevId: 439332549 --- .../java/androidx/media3/transformer/SsimHelper.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java index 5b00cd21b4..dc489d00fe 100644 --- a/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java +++ b/libraries/transformer/src/androidTest/java/androidx/media3/transformer/SsimHelper.java @@ -330,6 +330,17 @@ public final class SsimHelper { private static final double C2 = pow(IMAGE_DYNAMIC_RANGE * K2, 2); private static final int WINDOW_SIZE = 8; + /** + * Calculates the Structural Similarity Index (SSIM) between two images. + * + * @param expected The luminance channel (Y) bitmap of the expected image. + * @param actual The luminance channel (Y) bitmap of the actual image. + * @param offset The offset. + * @param stride The stride of the bitmap. + * @param width The image width in pixels. + * @param height The image height in pixels. + * @return The SSIM score between the input images. + */ public static double calculate( int[] expected, int[] actual, int offset, int stride, int width, int height) { double totalSsim = 0; From e780a32de43911e354d9adedae36a86a0fefa7c7 Mon Sep 17 00:00:00 2001 From: claincly Date: Mon, 4 Apr 2022 17:19:35 +0100 Subject: [PATCH 052/116] Support colon (:) in RTSP timing. Some RTSP servers use `npt`: notation rather than `npt=` PiperOrigin-RevId: 439333319 --- .../androidx/media3/exoplayer/rtsp/RtspSessionTiming.java | 3 ++- .../media3/exoplayer/rtsp/RtspSessionTimingTest.java | 7 +++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java index 60ff5a9101..a3493e9525 100644 --- a/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java +++ b/libraries/exoplayer_rtsp/src/main/java/androidx/media3/exoplayer/rtsp/RtspSessionTiming.java @@ -38,8 +38,9 @@ import java.util.regex.Pattern; new RtspSessionTiming(/* startTimeMs= */ 0, /* stopTimeMs= */ C.TIME_UNSET); // We only support npt=xxx-[xxx], but not npt=-xxx. See RFC2326 Section 3.6. + // Supports both npt= and npt: identifier. private static final Pattern NPT_RANGE_PATTERN = - Pattern.compile("npt=([.\\d]+|now)\\s?-\\s?([.\\d]+)?"); + Pattern.compile("npt[:=]([.\\d]+|now)\\s?-\\s?([.\\d]+)?"); private static final String START_TIMING_NTP_FORMAT = "npt=%.3f-"; private static final long LIVE_START_TIME = 0; diff --git a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java index ad7c05fee0..e9ac8cf287 100644 --- a/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java +++ b/libraries/exoplayer_rtsp/src/test/java/androidx/media3/exoplayer/rtsp/RtspSessionTimingTest.java @@ -54,6 +54,13 @@ public class RtspSessionTimingTest { assertThat(sessionTiming.isLive()).isFalse(); } + @Test + public void parseTiming_withRangeTimingAndColonSeparator() throws Exception { + RtspSessionTiming sessionTiming = RtspSessionTiming.parseTiming("npt:0.000-32.054"); + assertThat(sessionTiming.getDurationMs()).isEqualTo(32054); + assertThat(sessionTiming.isLive()).isFalse(); + } + @Test public void parseTiming_withInvalidRangeTiming_throwsIllegalArgumentException() { assertThrows( From 59f87a513434fedb31a425305f3f39048ab84b2b Mon Sep 17 00:00:00 2001 From: huangdarwin Date: Mon, 4 Apr 2022 18:54:35 +0100 Subject: [PATCH 053/116] Transformer Demo: Disable SDR button for API <31. This makes it more clear to users of the demo that this is only available under API 31. PiperOrigin-RevId: 439358674 --- .../demo/transformer/ConfigurationActivity.java | 12 ++++++++++-- demos/transformer/src/main/res/values/strings.xml | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index 7f87af30fc..0245b9cc60 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -161,6 +161,8 @@ public final class ConfigurationActivity extends AppCompatActivity { enableFallbackCheckBox = findViewById(R.id.enable_fallback_checkbox); enableRequestSdrToneMappingCheckBox = findViewById(R.id.request_sdr_tone_mapping_checkbox); + enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); + findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); } @@ -303,7 +305,8 @@ public final class ConfigurationActivity extends AppCompatActivity { resolutionHeightSpinner.setEnabled(isVideoEnabled); scaleSpinner.setEnabled(isVideoEnabled); rotateSpinner.setEnabled(isVideoEnabled); - enableRequestSdrToneMappingCheckBox.setEnabled(isVideoEnabled); + enableRequestSdrToneMappingCheckBox.setEnabled( + isRequestSdrToneMappingSupported() && isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); @@ -311,7 +314,12 @@ public final class ConfigurationActivity extends AppCompatActivity { findViewById(R.id.resolution_height_text_view).setEnabled(isVideoEnabled); findViewById(R.id.scale).setEnabled(isVideoEnabled); findViewById(R.id.rotate).setEnabled(isVideoEnabled); - findViewById(R.id.request_sdr_tone_mapping).setEnabled(isVideoEnabled); + findViewById(R.id.request_sdr_tone_mapping) + .setEnabled(isRequestSdrToneMappingSupported() && isVideoEnabled); findViewById(R.id.hdr_editing).setEnabled(isVideoEnabled); } + + private static boolean isRequestSdrToneMappingSupported() { + return Util.SDK_INT >= 31; + } } diff --git a/demos/transformer/src/main/res/values/strings.xml b/demos/transformer/src/main/res/values/strings.xml index 8ab9fe25f2..c01a06bd3a 100644 --- a/demos/transformer/src/main/res/values/strings.xml +++ b/demos/transformer/src/main/res/values/strings.xml @@ -27,7 +27,7 @@ Scale video Rotate video (degrees) Enable fallback - Request SDR tone-mapping + Request SDR tone-mapping (API 31+) [Experimental] HDR editing Transform Debug preview: From 296efbb395ca218ee941210c7ebf9710e2bd9e26 Mon Sep 17 00:00:00 2001 From: olly Date: Tue, 5 Apr 2022 01:44:09 +0100 Subject: [PATCH 054/116] Reading AV1 initialization data. We add an entire class like we do for parsing other codec initialization formats; it's currently not doing any parsing though (... initialization data is really simple for AV1 though: just the entire contents of the box). For testing, we add the sample file, having been re-encoded with ffmpeg (and we also happen to have another av1 file, too). PiperOrigin-RevId: 439453823 --- RELEASENOTES.md | 1 + .../media3/extractor/mp4/AtomParsers.java | 5 + .../extractor/mp4/Mp4ExtractorTest.java | 6 + .../extractordumps/mp4/sample_av1.mp4.0.dump | 337 ++++++++++++++++++ .../extractordumps/mp4/sample_av1.mp4.1.dump | 285 +++++++++++++++ .../extractordumps/mp4/sample_av1.mp4.2.dump | 221 ++++++++++++ .../extractordumps/mp4/sample_av1.mp4.3.dump | 161 +++++++++ .../mp4/sample_av1.mp4.unknown_length.dump | 337 ++++++++++++++++++ .../sample_with_colr_mdcv_and_clli.mp4.0.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.1.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.2.dump | 2 + .../sample_with_colr_mdcv_and_clli.mp4.3.dump | 2 + ...colr_mdcv_and_clli.mp4.unknown_length.dump | 2 + .../src/test/assets/media/mp4/sample_av1.mp4 | Bin 0 -> 91219 bytes 14 files changed, 1363 insertions(+) create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump create mode 100644 libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump create mode 100644 libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 diff --git a/RELEASENOTES.md b/RELEASENOTES.md index b7507f8d46..4e0e3c5aeb 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -25,6 +25,7 @@ * Extractors: * Matroska: Parse `DiscardPadding` for Opus tracks. * Parse bitrates from `esds` boxes. + * MP4: Parse initialization data from AV1 tracks. * UI: * Fix delivery of events to `OnClickListener`s set on `PlayerView` and `LegacyPlayerView`, in the case that `useController=false` diff --git a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java index 05511fc7e5..56acf9513b 100644 --- a/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java +++ b/libraries/extractor/src/main/java/androidx/media3/extractor/mp4/AtomParsers.java @@ -1169,6 +1169,11 @@ import org.checkerframework.checker.nullness.compatqual.NullableType; } else if (childAtomType == Atom.TYPE_av1C) { ExtractorUtil.checkContainerInput(mimeType == null, /* message= */ null); mimeType = MimeTypes.VIDEO_AV1; + + int childAtomBodySize = childAtomSize - Atom.HEADER_SIZE; + byte[] onlyInitializationDataChunk = new byte[childAtomBodySize]; + parent.readBytes(onlyInitializationDataChunk, /* offset= */ 0, childAtomBodySize); + initializationData = ImmutableList.of(onlyInitializationDataChunk); } else if (childAtomType == Atom.TYPE_clli) { if (hdrStaticInfo == null) { hdrStaticInfo = allocateHdrStaticInfo(); diff --git a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java index b42d0f670e..8b09e446bb 100644 --- a/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java +++ b/libraries/extractor/src/test/java/androidx/media3/extractor/mp4/Mp4ExtractorTest.java @@ -97,6 +97,12 @@ public final class Mp4ExtractorTest { Mp4Extractor::new, "media/mp4/sample_mpegh_mhm1.mp4", simulationConfig); } + @Test + public void mp4SampleWithAv1Track() throws Exception { + ExtractorAsserts.assertBehavior( + Mp4Extractor::new, "media/mp4/sample_av1.mp4", simulationConfig); + } + @Test public void mp4SampleWithColorInfo() throws Exception { ExtractorAsserts.assertBehavior( diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump new file mode 100644 index 0000000000..e9a5eb797e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.0.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 43000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 66219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 89439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 112659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 135879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 159099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 182319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 205539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 228759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 251979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 275199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 298419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 321639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump new file mode 100644 index 0000000000..95bc8841af --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.1.dump @@ -0,0 +1,285 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 7235 + sample count = 32 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 1: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 2: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 3: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 4: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 5: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 6: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 7: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 8: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 9: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 10: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 11: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 12: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 13: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 14: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 15: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 16: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 17: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 18: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 19: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 20: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 21: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 22: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 23: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 24: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 25: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 26: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 27: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 28: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 29: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 30: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 31: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump new file mode 100644 index 0000000000..c814ddc79d --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.2.dump @@ -0,0 +1,221 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 3545 + sample count = 16 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 1: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 2: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 3: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 4: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 5: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 6: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 7: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 8: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 9: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 10: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 11: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 12: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 13: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 14: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 15: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump new file mode 100644 index 0000000000..8dfd47721c --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.3.dump @@ -0,0 +1,161 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 6 + sample count = 1 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump new file mode 100644 index 0000000000..e9a5eb797e --- /dev/null +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_av1.mp4.unknown_length.dump @@ -0,0 +1,337 @@ +seekMap: + isSeekable = true + duration = 1089000 + getPosition(0) = [[timeUs=0, position=48]] + getPosition(1) = [[timeUs=0, position=48]] + getPosition(544500) = [[timeUs=0, position=48]] + getPosition(1089000) = [[timeUs=0, position=48]] +numberOfTracks = 2 +track 0: + total output bytes = 79444 + sample count = 30 + format 0: + id = 1 + sampleMimeType = video/av01 + maxInputSize = 54267 + width = 1080 + height = 720 + frameRate = 29.970028 + initializationData: + data = length 17, hash 54AC4E6D + sample 0: + time = 0 + flags = 1 + data = length 54237, hash 978897A5 + sample 1: + time = 33366 + flags = 0 + data = length 21903, hash D3A1A794 + sample 2: + time = 66733 + flags = 0 + data = length 65, hash 401C922E + sample 3: + time = 100100 + flags = 0 + data = length 3, hash D5E0 + sample 4: + time = 133466 + flags = 0 + data = length 161, hash 3BAF4398 + sample 5: + time = 166833 + flags = 0 + data = length 3, hash D610 + sample 6: + time = 200200 + flags = 0 + data = length 47, hash 1BF8FBF + sample 7: + time = 233566 + flags = 0 + data = length 3, hash D5D0 + sample 8: + time = 266933 + flags = 0 + data = length 287, hash AF180C67 + sample 9: + time = 300300 + flags = 0 + data = length 33, hash B4D41A8F + sample 10: + time = 333666 + flags = 0 + data = length 3, hash D5E0 + sample 11: + time = 367033 + flags = 0 + data = length 236, hash 4DEB22C9 + sample 12: + time = 400400 + flags = 0 + data = length 3, hash D600 + sample 13: + time = 433766 + flags = 0 + data = length 202, hash 6AF564D + sample 14: + time = 467133 + flags = 0 + data = length 3, hash D5C0 + sample 15: + time = 500500 + flags = 0 + data = length 1275, hash 9C2CCEA5 + sample 16: + time = 533866 + flags = 0 + data = length 103, hash AC226D96 + sample 17: + time = 567233 + flags = 0 + data = length 3, hash D5E0 + sample 18: + time = 600600 + flags = 0 + data = length 250, hash 1C73058F + sample 19: + time = 633966 + flags = 0 + data = length 3, hash D5F0 + sample 20: + time = 667333 + flags = 0 + data = length 39, hash 26EBA81E + sample 21: + time = 700700 + flags = 0 + data = length 3, hash D610 + sample 22: + time = 734066 + flags = 0 + data = length 289, hash 4E5480FB + sample 23: + time = 767433 + flags = 0 + data = length 45, hash 8A594F0A + sample 24: + time = 800800 + flags = 0 + data = length 3, hash D600 + sample 25: + time = 834166 + flags = 0 + data = length 116, hash 9D1150FF + sample 26: + time = 867533 + flags = 0 + data = length 3, hash D5F0 + sample 27: + time = 900900 + flags = 0 + data = length 33, hash C36E3AEF + sample 28: + time = 934266 + flags = 0 + data = length 26, hash 6119E4D3 + sample 29: + time = 967633 + flags = 536870912 + data = length 64, hash A8A201F0 +track 1: + total output bytes = 9529 + sample count = 45 + format 0: + averageBitrate = 72956 + peakBitrate = 74502 + id = 2 + sampleMimeType = audio/mp4a-latm + codecs = mp4a.40.2 + maxInputSize = 294 + channelCount = 1 + sampleRate = 44100 + language = und + metadata = entries=[TSSE: description=null: value=Lavf58.76.100] + initializationData: + data = length 2, hash 5F7 + sample 0: + time = 43000 + flags = 1 + data = length 23, hash 47DE9131 + sample 1: + time = 66219 + flags = 1 + data = length 6, hash 31EC5206 + sample 2: + time = 89439 + flags = 1 + data = length 148, hash 894A176B + sample 3: + time = 112659 + flags = 1 + data = length 189, hash CEF235A1 + sample 4: + time = 135879 + flags = 1 + data = length 205, hash BBF5F7B0 + sample 5: + time = 159099 + flags = 1 + data = length 210, hash F278B193 + sample 6: + time = 182319 + flags = 1 + data = length 210, hash 82DA1589 + sample 7: + time = 205539 + flags = 1 + data = length 207, hash 5BE231DF + sample 8: + time = 228759 + flags = 1 + data = length 225, hash 18819EE1 + sample 9: + time = 251979 + flags = 1 + data = length 215, hash CA7FA67B + sample 10: + time = 275199 + flags = 1 + data = length 211, hash 581A1C18 + sample 11: + time = 298419 + flags = 1 + data = length 216, hash ADB88187 + sample 12: + time = 321639 + flags = 1 + data = length 229, hash 2E8BA4DC + sample 13: + time = 344859 + flags = 1 + data = length 232, hash 22F0C510 + sample 14: + time = 368079 + flags = 1 + data = length 235, hash 867AD0DC + sample 15: + time = 391299 + flags = 1 + data = length 231, hash 84E823A8 + sample 16: + time = 414519 + flags = 1 + data = length 226, hash 1BEF3A95 + sample 17: + time = 437739 + flags = 1 + data = length 216, hash EAA345AE + sample 18: + time = 460959 + flags = 1 + data = length 229, hash 6957411F + sample 19: + time = 484179 + flags = 1 + data = length 219, hash 41275022 + sample 20: + time = 507399 + flags = 1 + data = length 241, hash 6495DF96 + sample 21: + time = 530619 + flags = 1 + data = length 228, hash 63D95906 + sample 22: + time = 553839 + flags = 1 + data = length 238, hash 34F676F9 + sample 23: + time = 577058 + flags = 1 + data = length 234, hash E5CBC045 + sample 24: + time = 600278 + flags = 1 + data = length 231, hash 5FC43661 + sample 25: + time = 623498 + flags = 1 + data = length 217, hash 682708ED + sample 26: + time = 646718 + flags = 1 + data = length 239, hash D43780FC + sample 27: + time = 669938 + flags = 1 + data = length 243, hash C5E17980 + sample 28: + time = 693158 + flags = 1 + data = length 231, hash AC5837BA + sample 29: + time = 716378 + flags = 1 + data = length 230, hash 169EE895 + sample 30: + time = 739598 + flags = 1 + data = length 238, hash C48FF3F1 + sample 31: + time = 762818 + flags = 1 + data = length 225, hash 531E4599 + sample 32: + time = 786038 + flags = 1 + data = length 232, hash CB3C6B8D + sample 33: + time = 809258 + flags = 1 + data = length 243, hash F8C94C7 + sample 34: + time = 832478 + flags = 1 + data = length 232, hash A646A7D0 + sample 35: + time = 855698 + flags = 1 + data = length 237, hash E8B787A5 + sample 36: + time = 878918 + flags = 1 + data = length 228, hash 3FA7A29F + sample 37: + time = 902138 + flags = 1 + data = length 235, hash B9B33B0A + sample 38: + time = 925358 + flags = 1 + data = length 264, hash 71A4869E + sample 39: + time = 948578 + flags = 1 + data = length 257, hash D049B54C + sample 40: + time = 971798 + flags = 1 + data = length 227, hash 66757231 + sample 41: + time = 995018 + flags = 1 + data = length 227, hash BD374F1B + sample 42: + time = 1018238 + flags = 1 + data = length 235, hash 999477F6 + sample 43: + time = 1041458 + flags = 1 + data = length 229, hash FFF98DF0 + sample 44: + time = 1064678 + flags = 536870913 + data = length 6, hash 31B22286 +tracksEnded = true diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump index 8c1813ef83..edf4002855 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.0.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump index 5011cfa353..2408a32712 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.1.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump index ad7c5fbe40..98312fac0e 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.2.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump index 9e8fbd7584..95a2ed41bd 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.3.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump index 8c1813ef83..edf4002855 100644 --- a/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump +++ b/libraries/test_data/src/test/assets/extractordumps/mp4/sample_with_colr_mdcv_and_clli.mp4.unknown_length.dump @@ -21,6 +21,8 @@ track 0: colorRange = 2 colorTransfer = 6 hdrStaticInfo = length 25, hash 423AFC35 + initializationData: + data = length 20, hash 4DF5B288 sample 0: time = 0 flags = 1 diff --git a/libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 b/libraries/test_data/src/test/assets/media/mp4/sample_av1.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..eaa6a1ecf80bbbb12ce02963960cd0f3b80c4a73 GIT binary patch literal 91219 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 Date: Tue, 5 Apr 2022 17:21:59 +0100 Subject: [PATCH 055/116] Fix typo in AdvancedFrameProcessorTest. PiperOrigin-RevId: 439599201 --- .../androidx/media3/transformer/AdvancedFrameProcessorTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java index bd4e16d567..c7e9f9b2b1 100644 --- a/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java +++ b/libraries/transformer/src/test/java/androidx/media3/transformer/AdvancedFrameProcessorTest.java @@ -39,7 +39,7 @@ public final class AdvancedFrameProcessorTest { } @Test - public void construct_withValidMatrixSize_completesSucessfully() { + public void construct_withValidMatrixSize_completesSuccessfully() { new AdvancedFrameProcessor(getApplicationContext(), new float[16]); } } From c235e4f4474af463b01f325afee26f12f89e02f2 Mon Sep 17 00:00:00 2001 From: hschlueter Date: Wed, 6 Apr 2022 11:28:55 +0100 Subject: [PATCH 056/116] Add matrix provider for AdvancedFrameProcessor and examples in demo. The matrix provider allows the transformation matrix to be updated for each frame based on the timestamp. The following example effects using this were added to the demo: * a zoom-in transition for the start of the video, * cropping a rotating rectangular frame portion, * rotating the frame around the y-axis in 3D. PiperOrigin-RevId: 439791592 --- .../AdvancedFrameProcessorFactory.java | 96 +++++++++++++++++++ .../transformer/ConfigurationActivity.java | 61 ++++++++---- .../demo/transformer/TransformerActivity.java | 23 +++++ .../res/layout/configuration_activity.xml | 23 +++-- .../src/main/res/values/strings.xml | 3 +- .../transformer/AdvancedFrameProcessor.java | 70 +++++++++++--- 6 files changed, 241 insertions(+), 35 deletions(-) create mode 100644 demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java new file mode 100644 index 0000000000..51214ebc51 --- /dev/null +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/AdvancedFrameProcessorFactory.java @@ -0,0 +1,96 @@ +/* + * Copyright 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package androidx.media3.demo.transformer; + +import android.content.Context; +import android.graphics.Matrix; +import androidx.media3.common.C; +import androidx.media3.common.util.Util; +import androidx.media3.transformer.AdvancedFrameProcessor; +import androidx.media3.transformer.GlFrameProcessor; + +/** + * Factory for {@link GlFrameProcessor GlFrameProcessors} that create video effects by applying + * transformation matrices to the individual video frames using {@link AdvancedFrameProcessor}. + */ +/* package */ final class AdvancedFrameProcessorFactory { + /** + * Returns a {@link GlFrameProcessor} that rescales the frames over the first {@value + * #ZOOM_DURATION_SECONDS} seconds, such that the rectangle filled with the input frame increases + * linearly in size from a single point to filling the full output frame. + */ + public static GlFrameProcessor createZoomInTransitionFrameProcessor(Context context) { + return new AdvancedFrameProcessor( + context, + /* matrixProvider= */ AdvancedFrameProcessorFactory::calculateZoomInTransitionMatrix); + } + + /** + * Returns a {@link GlFrameProcessor} that crops frames to a rectangle that moves on an ellipse. + */ + public static GlFrameProcessor createDizzyCropFrameProcessor(Context context) { + return new AdvancedFrameProcessor( + context, /* matrixProvider= */ AdvancedFrameProcessorFactory::calculateDizzyCropMatrix); + } + + /** + * Returns a {@link GlFrameProcessor} that rotates a frame in 3D around the y-axis and applies + * perspective projection to 2D. + */ + public static GlFrameProcessor createSpin3dFrameProcessor(Context context) { + return new AdvancedFrameProcessor( + context, /* matrixProvider= */ AdvancedFrameProcessorFactory::calculate3dSpinMatrix); + } + + private static final float ZOOM_DURATION_SECONDS = 2f; + private static final float DIZZY_CROP_ROTATION_PERIOD_US = 1_500_000f; + + private static Matrix calculateZoomInTransitionMatrix(long presentationTimeUs) { + Matrix transformationMatrix = new Matrix(); + float scale = Math.min(1, presentationTimeUs / (C.MICROS_PER_SECOND * ZOOM_DURATION_SECONDS)); + transformationMatrix.postScale(/* sx= */ scale, /* sy= */ scale); + return transformationMatrix; + } + + private static android.graphics.Matrix calculateDizzyCropMatrix(long presentationTimeUs) { + double theta = presentationTimeUs * 2 * Math.PI / DIZZY_CROP_ROTATION_PERIOD_US; + float centerX = 0.5f * (float) Math.cos(theta); + float centerY = 0.5f * (float) Math.sin(theta); + android.graphics.Matrix transformationMatrix = new android.graphics.Matrix(); + transformationMatrix.postTranslate(/* dx= */ centerX, /* dy= */ centerY); + transformationMatrix.postScale(/* sx= */ 2f, /* sy= */ 2f); + return transformationMatrix; + } + + private static float[] calculate3dSpinMatrix(long presentationTimeUs) { + float[] transformationMatrix = new float[16]; + android.opengl.Matrix.frustumM( + transformationMatrix, + /* offset= */ 0, + /* left= */ -1f, + /* right= */ 1f, + /* bottom= */ -1f, + /* top= */ 1f, + /* near= */ 3f, + /* far= */ 5f); + android.opengl.Matrix.translateM( + transformationMatrix, /* mOffset= */ 0, /* x= */ 0f, /* y= */ 0f, /* z= */ -4f); + float theta = Util.usToMs(presentationTimeUs) / 10f; + android.opengl.Matrix.rotateM( + transformationMatrix, /* mOffset= */ 0, theta, /* x= */ 0f, /* y= */ 1f, /* z= */ 0f); + return transformationMatrix; + } +} diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java index 0245b9cc60..6182f3bdd1 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/ConfigurationActivity.java @@ -56,6 +56,7 @@ public final class ConfigurationActivity extends AppCompatActivity { public static final String ENABLE_FALLBACK = "enable_fallback"; public static final String ENABLE_REQUEST_SDR_TONE_MAPPING = "enable_request_sdr_tone_mapping"; public static final String ENABLE_HDR_EDITING = "enable_hdr_editing"; + public static final String FRAME_PROCESSOR_SELECTION = "frame_processor_selection"; private static final String[] INPUT_URIS = { "https://html5demos.com/assets/dizzy.mp4", "https://storage.googleapis.com/exoplayer-test-media-0/android-block-1080-hevc.mp4", @@ -80,10 +81,11 @@ public final class ConfigurationActivity extends AppCompatActivity { "SEF slow motion with 240 fps", "MP4 with HDR (HDR10) H265 video (encoding may fail)", }; + private static final String[] FRAME_PROCESSORS = {"Dizzy crop", "3D spin", "Zoom in start"}; private static final String SAME_AS_INPUT_OPTION = "same as input"; - private @MonotonicNonNull Button chooseFileButton; - private @MonotonicNonNull TextView chosenFileTextView; + private @MonotonicNonNull Button selectFileButton; + private @MonotonicNonNull TextView selectedFileTextView; private @MonotonicNonNull CheckBox removeAudioCheckbox; private @MonotonicNonNull CheckBox removeVideoCheckbox; private @MonotonicNonNull CheckBox flattenForSlowMotionCheckbox; @@ -95,6 +97,8 @@ public final class ConfigurationActivity extends AppCompatActivity { private @MonotonicNonNull CheckBox enableFallbackCheckBox; private @MonotonicNonNull CheckBox enableRequestSdrToneMappingCheckBox; private @MonotonicNonNull CheckBox enableHdrEditingCheckBox; + private @MonotonicNonNull Button selectFrameProcessorsButton; + private boolean @MonotonicNonNull [] selectedFrameProcessors; private int inputUriPosition; @Override @@ -104,11 +108,11 @@ public final class ConfigurationActivity extends AppCompatActivity { findViewById(R.id.transform_button).setOnClickListener(this::startTransformation); - chooseFileButton = findViewById(R.id.choose_file_button); - chooseFileButton.setOnClickListener(this::chooseFile); + selectFileButton = findViewById(R.id.select_file_button); + selectFileButton.setOnClickListener(this::selectFile); - chosenFileTextView = findViewById(R.id.chosen_file_text_view); - chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView = findViewById(R.id.selected_file_text_view); + selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); removeAudioCheckbox = findViewById(R.id.remove_audio_checkbox); removeAudioCheckbox.setOnClickListener(this::onRemoveAudio); @@ -164,6 +168,10 @@ public final class ConfigurationActivity extends AppCompatActivity { enableRequestSdrToneMappingCheckBox.setEnabled(isRequestSdrToneMappingSupported()); findViewById(R.id.request_sdr_tone_mapping).setEnabled(isRequestSdrToneMappingSupported()); enableHdrEditingCheckBox = findViewById(R.id.hdr_editing_checkbox); + + selectedFrameProcessors = new boolean[FRAME_PROCESSORS.length]; + selectFrameProcessorsButton = findViewById(R.id.select_frameprocessors_button); + selectFrameProcessorsButton.setOnClickListener(this::selectFrameProcessors); } @Override @@ -171,8 +179,8 @@ public final class ConfigurationActivity extends AppCompatActivity { super.onResume(); @Nullable Uri intentUri = getIntent().getData(); if (intentUri != null) { - checkNotNull(chooseFileButton).setEnabled(false); - checkNotNull(chosenFileTextView).setText(intentUri.toString()); + checkNotNull(selectFileButton).setEnabled(false); + checkNotNull(selectedFileTextView).setText(intentUri.toString()); } } @@ -193,7 +201,8 @@ public final class ConfigurationActivity extends AppCompatActivity { "rotateSpinner", "enableFallbackCheckBox", "enableRequestSdrToneMappingCheckBox", - "enableHdrEditingCheckBox" + "enableHdrEditingCheckBox", + "selectedFrameProcessors" }) private void startTransformation(View view) { Intent transformerIntent = new Intent(this, TransformerActivity.class); @@ -228,6 +237,7 @@ public final class ConfigurationActivity extends AppCompatActivity { bundle.putBoolean( ENABLE_REQUEST_SDR_TONE_MAPPING, enableRequestSdrToneMappingCheckBox.isChecked()); bundle.putBoolean(ENABLE_HDR_EDITING, enableHdrEditingCheckBox.isChecked()); + bundle.putBooleanArray(FRAME_PROCESSOR_SELECTION, selectedFrameProcessors); transformerIntent.putExtras(bundle); @Nullable Uri intentUri = getIntent().getData(); @@ -237,19 +247,34 @@ public final class ConfigurationActivity extends AppCompatActivity { startActivity(transformerIntent); } - private void chooseFile(View view) { + private void selectFile(View view) { new AlertDialog.Builder(/* context= */ this) - .setTitle(R.string.choose_file_title) + .setTitle(R.string.select_file_title) .setSingleChoiceItems(URI_DESCRIPTIONS, inputUriPosition, this::selectFileInDialog) .setPositiveButton(android.R.string.ok, /* listener= */ null) .create() .show(); } - @RequiresNonNull("chosenFileTextView") + private void selectFrameProcessors(View view) { + new AlertDialog.Builder(/* context= */ this) + .setTitle(R.string.select_frameprocessors) + .setMultiChoiceItems( + FRAME_PROCESSORS, checkNotNull(selectedFrameProcessors), this::selectFrameProcessor) + .setPositiveButton(android.R.string.ok, /* listener= */ null) + .create() + .show(); + } + + @RequiresNonNull("selectedFileTextView") private void selectFileInDialog(DialogInterface dialog, int which) { inputUriPosition = which; - chosenFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + selectedFileTextView.setText(URI_DESCRIPTIONS[inputUriPosition]); + } + + @RequiresNonNull("selectedFrameProcessors") + private void selectFrameProcessor(DialogInterface dialog, int which, boolean isChecked) { + selectedFrameProcessors[which] = isChecked; } @RequiresNonNull({ @@ -260,7 +285,8 @@ public final class ConfigurationActivity extends AppCompatActivity { "scaleSpinner", "rotateSpinner", "enableRequestSdrToneMappingCheckBox", - "enableHdrEditingCheckBox" + "enableHdrEditingCheckBox", + "selectFrameProcessorsButton" }) private void onRemoveAudio(View view) { if (((CheckBox) view).isChecked()) { @@ -279,7 +305,8 @@ public final class ConfigurationActivity extends AppCompatActivity { "scaleSpinner", "rotateSpinner", "enableRequestSdrToneMappingCheckBox", - "enableHdrEditingCheckBox" + "enableHdrEditingCheckBox", + "selectFrameProcessorsButton" }) private void onRemoveVideo(View view) { if (((CheckBox) view).isChecked()) { @@ -297,7 +324,8 @@ public final class ConfigurationActivity extends AppCompatActivity { "scaleSpinner", "rotateSpinner", "enableRequestSdrToneMappingCheckBox", - "enableHdrEditingCheckBox" + "enableHdrEditingCheckBox", + "selectFrameProcessorsButton" }) private void enableTrackSpecificOptions(boolean isAudioEnabled, boolean isVideoEnabled) { audioMimeSpinner.setEnabled(isAudioEnabled); @@ -308,6 +336,7 @@ public final class ConfigurationActivity extends AppCompatActivity { enableRequestSdrToneMappingCheckBox.setEnabled( isRequestSdrToneMappingSupported() && isVideoEnabled); enableHdrEditingCheckBox.setEnabled(isVideoEnabled); + selectFrameProcessorsButton.setEnabled(isVideoEnabled); findViewById(R.id.audio_mime_text_view).setEnabled(isAudioEnabled); findViewById(R.id.video_mime_text_view).setEnabled(isVideoEnabled); diff --git a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java index 3a957cba3e..cb476adef1 100644 --- a/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java +++ b/demos/transformer/src/main/java/androidx/media3/demo/transformer/TransformerActivity.java @@ -40,6 +40,7 @@ import androidx.media3.exoplayer.ExoPlayer; import androidx.media3.exoplayer.util.DebugTextViewHelper; import androidx.media3.transformer.DefaultEncoderFactory; import androidx.media3.transformer.EncoderSelector; +import androidx.media3.transformer.GlFrameProcessor; import androidx.media3.transformer.ProgressHolder; import androidx.media3.transformer.TransformationException; import androidx.media3.transformer.TransformationRequest; @@ -50,6 +51,7 @@ import androidx.media3.ui.PlayerView; import com.google.android.material.progressindicator.LinearProgressIndicator; import com.google.common.base.Stopwatch; import com.google.common.base.Ticker; +import com.google.common.collect.ImmutableList; import java.io.File; import java.io.IOException; import java.util.concurrent.CountDownLatch; @@ -237,6 +239,27 @@ public final class TransformerActivity extends AppCompatActivity { new DefaultEncoderFactory( EncoderSelector.DEFAULT, /* enableFallback= */ bundle.getBoolean(ConfigurationActivity.ENABLE_FALLBACK))); + + ImmutableList.Builder frameProcessors = new ImmutableList.Builder<>(); + @Nullable + boolean[] selectedFrameProcessors = + bundle.getBooleanArray(ConfigurationActivity.FRAME_PROCESSOR_SELECTION); + if (selectedFrameProcessors != null) { + if (selectedFrameProcessors[0]) { + frameProcessors.add( + AdvancedFrameProcessorFactory.createDizzyCropFrameProcessor(/* context= */ this)); + } + if (selectedFrameProcessors[1]) { + frameProcessors.add( + AdvancedFrameProcessorFactory.createSpin3dFrameProcessor(/* context= */ this)); + } + if (selectedFrameProcessors[2]) { + frameProcessors.add( + AdvancedFrameProcessorFactory.createZoomInTransitionFrameProcessor( + /* context= */ this)); + } + transformerBuilder.setFrameProcessors(frameProcessors.build()); + } } return transformerBuilder .addListener( diff --git a/demos/transformer/src/main/res/layout/configuration_activity.xml b/demos/transformer/src/main/res/layout/configuration_activity.xml index c973bf4137..7464ce9f6b 100644 --- a/demos/transformer/src/main/res/layout/configuration_activity.xml +++ b/demos/transformer/src/main/res/layout/configuration_activity.xml @@ -34,18 +34,18 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" />