From 5a3340d638cfeb3809f435b1b0118f2acce15079 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Fri, 12 Dec 2014 14:18:44 +0000 Subject: [PATCH] Add initial AC3 passthrough support. --- .../demo/full/FullPlayerActivity.java | 28 +- .../demo/full/player/DashRendererBuilder.java | 32 +- .../demo/full/player/DemoPlayer.java | 5 +- .../Ac3PassthroughAudioTrackRenderer.java | 316 ++++++++++++++++++ 4 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java index 492c1ab0ae..d32c61fd22 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/FullPlayerActivity.java @@ -17,6 +17,8 @@ package com.google.android.exoplayer.demo.full; import com.google.android.exoplayer.ExoPlayer; import com.google.android.exoplayer.VideoSurfaceView; +import com.google.android.exoplayer.audio.AudioCapabilities; +import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; import com.google.android.exoplayer.demo.DemoUtil; import com.google.android.exoplayer.demo.R; import com.google.android.exoplayer.demo.full.player.DashRendererBuilder; @@ -59,7 +61,7 @@ import android.widget.Toast; * An activity that plays media using {@link DemoPlayer}. */ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callback, OnClickListener, - DemoPlayer.Listener, DemoPlayer.TextListener { + DemoPlayer.Listener, DemoPlayer.TextListener, AudioCapabilitiesReceiver.Listener { private static final float CAPTION_LINE_HEIGHT_RATIO = 0.0533f; private static final int MENU_GROUP_TRACKS = 1; @@ -89,6 +91,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba private int contentType; private String contentId; + private AudioCapabilitiesReceiver audioCapabilitiesReceiver; + private AudioCapabilities audioCapabilities; + // Activity lifecycle @Override @@ -112,6 +117,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } }); + audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(getApplicationContext(), this); + shutterView = findViewById(R.id.shutter); debugRootView = findViewById(R.id.controls_root); @@ -137,7 +144,9 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba public void onResume() { super.onResume(); configureSubtitleView(); - preparePlayer(); + + // The player will be prepared on receiving audio capabilities. + audioCapabilitiesReceiver.register(); } @Override @@ -148,6 +157,8 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } else { player.blockingClearSurface(); } + + audioCapabilitiesReceiver.unregister(); } @Override @@ -166,6 +177,17 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba } } + // AudioCapabilitiesReceiver.Listener methods + + @Override + public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) { + this.audioCapabilities = audioCapabilities; + releasePlayer(); + + autoPlay = true; + preparePlayer(); + } + // Internal methods private RendererBuilder getRendererBuilder() { @@ -176,7 +198,7 @@ public class FullPlayerActivity extends Activity implements SurfaceHolder.Callba new SmoothStreamingTestMediaDrmCallback(), debugTextView); case DemoUtil.TYPE_DASH: return new DashRendererBuilder(userAgent, contentUri.toString(), contentId, - new WidevineTestMediaDrmCallback(contentId), debugTextView); + new WidevineTestMediaDrmCallback(contentId), debugTextView, audioCapabilities); default: return new DefaultRendererBuilder(this, contentUri, debugTextView); } diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java index 907175aea6..bbc9e868e1 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DashRendererBuilder.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.demo.full.player; +import com.google.android.exoplayer.Ac3PassthroughAudioTrackRenderer; import com.google.android.exoplayer.DefaultLoadControl; import com.google.android.exoplayer.LoadControl; import com.google.android.exoplayer.MediaCodecAudioTrackRenderer; @@ -22,6 +23,7 @@ import com.google.android.exoplayer.MediaCodecUtil; import com.google.android.exoplayer.MediaCodecVideoTrackRenderer; import com.google.android.exoplayer.SampleSource; import com.google.android.exoplayer.TrackRenderer; +import com.google.android.exoplayer.audio.AudioCapabilities; import com.google.android.exoplayer.chunk.ChunkSampleSource; import com.google.android.exoplayer.chunk.ChunkSource; import com.google.android.exoplayer.chunk.Format; @@ -84,18 +86,20 @@ public class DashRendererBuilder implements RendererBuilder, private final String contentId; private final MediaDrmCallback drmCallback; private final TextView debugTextView; + private final AudioCapabilities audioCapabilities; private DemoPlayer player; private RendererBuilderCallback callback; private ManifestFetcher manifestFetcher; public DashRendererBuilder(String userAgent, String url, String contentId, - MediaDrmCallback drmCallback, TextView debugTextView) { + MediaDrmCallback drmCallback, TextView debugTextView, AudioCapabilities audioCapabilities) { this.userAgent = userAgent; this.url = url; this.contentId = contentId; this.drmCallback = drmCallback; this.debugTextView = debugTextView; + this.audioCapabilities = audioCapabilities; } @Override @@ -208,6 +212,7 @@ public class DashRendererBuilder implements RendererBuilder, } // Build the audio chunk sources. + boolean haveAc3Tracks = false; List audioChunkSourceList = new ArrayList(); List audioTrackNameList = new ArrayList(); if (audioAdaptationSet != null) { @@ -220,6 +225,19 @@ public class DashRendererBuilder implements RendererBuilder, format.audioSamplingRate + "Hz)"); audioChunkSourceList.add(new DashChunkSource(manifestFetcher, audioAdaptationSetIndex, new int[] {i}, audioDataSource, audioEvaluator, LIVE_EDGE_LATENCY_MS)); + haveAc3Tracks |= format.mimeType.equals(MimeTypes.AUDIO_AC3) + || format.mimeType.equals(MimeTypes.AUDIO_EC3); + } + // Filter out non-AC-3 tracks if there is an AC-3 track, to avoid having to switch renderers. + if (haveAc3Tracks) { + for (int i = audioRepresentations.size() - 1; i >= 0; i--) { + Format format = audioRepresentations.get(i).format; + if (!format.mimeType.equals(MimeTypes.AUDIO_AC3) + && !format.mimeType.equals(MimeTypes.AUDIO_EC3)) { + audioTrackNameList.remove(i); + audioChunkSourceList.remove(i); + } + } } } @@ -238,8 +256,16 @@ public class DashRendererBuilder implements RendererBuilder, SampleSource audioSampleSource = new ChunkSampleSource(audioChunkSource, loadControl, AUDIO_BUFFER_SEGMENTS * BUFFER_SEGMENT_SIZE, true, mainHandler, player, DemoPlayer.TYPE_AUDIO); - audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, - mainHandler, player); + // TODO: There needs to be some logic to filter out non-AC3 tracks when selecting to use AC3. + boolean useAc3Passthrough = haveAc3Tracks && audioCapabilities != null + && (audioCapabilities.supportsAc3() || audioCapabilities.supportsEAc3()); + if (useAc3Passthrough) { + audioRenderer = + new Ac3PassthroughAudioTrackRenderer(audioSampleSource, mainHandler, player); + } else { + audioRenderer = new MediaCodecAudioTrackRenderer(audioSampleSource, drmSessionManager, true, + mainHandler, player); + } } // Build the text chunk sources. diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java index dfa900d18d..97ce5f5506 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/DemoPlayer.java @@ -15,6 +15,7 @@ */ package com.google.android.exoplayer.demo.full.player; +import com.google.android.exoplayer.Ac3PassthroughAudioTrackRenderer; import com.google.android.exoplayer.DummyTrackRenderer; import com.google.android.exoplayer.ExoPlaybackException; import com.google.android.exoplayer.ExoPlayer; @@ -45,8 +46,8 @@ import java.util.concurrent.CopyOnWriteArrayList; */ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventListener, DefaultBandwidthMeter.EventListener, MediaCodecVideoTrackRenderer.EventListener, - MediaCodecAudioTrackRenderer.EventListener, TextTrackRenderer.TextRenderer, - StreamingDrmSessionManager.EventListener { + MediaCodecAudioTrackRenderer.EventListener, Ac3PassthroughAudioTrackRenderer.EventListener, + TextTrackRenderer.TextRenderer, StreamingDrmSessionManager.EventListener { /** * Builds renderers for the player. diff --git a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java new file mode 100644 index 0000000000..bfbe3b56d3 --- /dev/null +++ b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2014 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 com.google.android.exoplayer; + +import com.google.android.exoplayer.audio.AudioCapabilitiesReceiver; +import com.google.android.exoplayer.audio.AudioTrack; +import com.google.android.exoplayer.util.Assertions; +import com.google.android.exoplayer.util.MimeTypes; + +import android.annotation.TargetApi; +import android.media.AudioFormat; +import android.os.Handler; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Renders encoded AC-3/enhanced AC-3 data to an {@link AudioTrack} for decoding on the playback + * device. + * + *

To determine whether the playback device supports passthrough, receive an audio configuration + * using {@link AudioCapabilitiesReceiver} and check whether the audio capabilities include + * AC-3/enhanced AC-3 passthrough. + */ +@TargetApi(21) +public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer { + + /** + * Interface definition for a callback to be notified of {@link Ac3PassthroughAudioTrackRenderer} + * events. + */ + public interface EventListener { + + /** + * Invoked when an {@link AudioTrack} fails to initialize. + * + * @param e The corresponding exception. + */ + void onAudioTrackInitializationError(AudioTrack.InitializationException e); + + } + + /** + * The type of a message that can be passed to an instance of this class via + * {@link ExoPlayer#sendMessage} or {@link ExoPlayer#blockingSendMessage}. The message object + * should be a {@link Float} with 0 being silence and 1 being unity gain. + */ + public static final int MSG_SET_VOLUME = 1; + + private static final int SOURCE_STATE_NOT_READY = 0; + private static final int SOURCE_STATE_READY = 1; + + /** Default buffer size for AC-3 packets from the sample source */ + private static final int DEFAULT_BUFFER_SIZE = 16384 * 2; + + /** Multiplication factor for the audio track's buffer size. */ + private static final int MIN_BUFFER_MULTIPLICATION_FACTOR = 3; + + private final Handler eventHandler; + private final EventListener eventListener; + + private final SampleSource source; + private final SampleHolder sampleHolder; + private final MediaFormatHolder formatHolder; + + private int trackIndex; + private MediaFormat format; + + private int sourceState; + private boolean inputStreamEnded; + private boolean shouldReadInputBuffer; + + private long currentPositionUs; + + private AudioTrack audioTrack; + private int audioSessionId; + + /** + * Constructs a new track renderer that passes AC-3 samples directly to an audio track. + * + * @param source The upstream source from which the renderer obtains samples. + * @param eventHandler A handler to use when delivering events to {@code eventListener}. May be + * null if delivery of events is not required. + * @param eventListener A listener of events. May be null if delivery of events is not required. + */ + public Ac3PassthroughAudioTrackRenderer( + SampleSource source, Handler eventHandler, EventListener eventListener) { + this.source = Assertions.checkNotNull(source); + this.eventHandler = eventHandler; + this.eventListener = eventListener; + sampleHolder = new SampleHolder(SampleHolder.BUFFER_REPLACEMENT_MODE_NORMAL); + sampleHolder.data = ByteBuffer.allocateDirect(DEFAULT_BUFFER_SIZE); + formatHolder = new MediaFormatHolder(); + audioTrack = new AudioTrack(MIN_BUFFER_MULTIPLICATION_FACTOR); + shouldReadInputBuffer = true; + } + + @Override + protected boolean isTimeSource() { + return true; + } + + @Override + protected int doPrepare() throws ExoPlaybackException { + try { + boolean sourcePrepared = source.prepare(); + if (!sourcePrepared) { + return TrackRenderer.STATE_UNPREPARED; + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + + for (int i = 0; i < source.getTrackCount(); i++) { + // TODO(andrewlewis): Choose best format here after checking playout formats from HDMI config. + if (handlesMimeType(source.getTrackInfo(i).mimeType)) { + trackIndex = i; + return TrackRenderer.STATE_PREPARED; + } + } + + return TrackRenderer.STATE_IGNORE; + } + + private static boolean handlesMimeType(String mimeType) { + return MimeTypes.AUDIO_AC3.equals(mimeType) || MimeTypes.AUDIO_EC3.equals(mimeType); + } + + @Override + protected void onEnabled(long positionUs, boolean joining) { + source.enable(trackIndex, positionUs); + sourceState = SOURCE_STATE_NOT_READY; + inputStreamEnded = false; + currentPositionUs = positionUs; + } + + @Override + protected void doSomeWork(long positionUs, long elapsedRealtimeUs) throws ExoPlaybackException { + try { + sourceState = source.continueBuffering(positionUs) + ? (sourceState == SOURCE_STATE_NOT_READY ? SOURCE_STATE_READY : sourceState) + : SOURCE_STATE_NOT_READY; + + if (format == null) { + readFormat(); + } else { + // Initialize and start the audio track now. + if (!audioTrack.isInitialized()) { + int oldAudioSessionId = audioSessionId; + try { + audioSessionId = audioTrack.initialize(oldAudioSessionId); + } catch (AudioTrack.InitializationException e) { + notifyAudioTrackInitializationError(e); + throw new ExoPlaybackException(e); + } + + if (getState() == TrackRenderer.STATE_STARTED) { + audioTrack.play(); + } + } + + feedInputBuffer(); + } + } catch (IOException e) { + throw new ExoPlaybackException(e); + } + } + + private void readFormat() throws IOException { + int result = source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); + if (result == SampleSource.FORMAT_READ) { + format = formatHolder.format; + // TODO: For E-AC-3 input, reconfigure with AudioFormat.ENCODING_E_AC3. + audioTrack.reconfigure(format.getFrameworkMediaFormatV16(), AudioFormat.ENCODING_AC3, 0); + } + } + + private void feedInputBuffer() throws IOException { + if (!audioTrack.isInitialized() || inputStreamEnded) { + return; + } + + // Get more data if we have run out. + if (shouldReadInputBuffer) { + sampleHolder.data.clear(); + + int result = + source.readData(trackIndex, currentPositionUs, formatHolder, sampleHolder, false); + sampleHolder.data.flip(); + shouldReadInputBuffer = false; + + if (result == SampleSource.FORMAT_READ) { + format = formatHolder.format; + } + if (result == SampleSource.END_OF_STREAM) { + inputStreamEnded = true; + } + if (result != SampleSource.SAMPLE_READ) { + return; + } + } + + int handleBufferResult = + audioTrack.handleBuffer(sampleHolder.data, 0, sampleHolder.size, sampleHolder.timeUs); + + // If we are out of sync, allow currentPositionUs to jump backwards. + if ((handleBufferResult & AudioTrack.RESULT_POSITION_DISCONTINUITY) != 0) { + currentPositionUs = Long.MIN_VALUE; + } + + // Get another input buffer if this one was consumed. + shouldReadInputBuffer = (handleBufferResult & AudioTrack.RESULT_BUFFER_CONSUMED) != 0; + } + + @Override + protected void onStarted() { + if (audioTrack.isInitialized()) { + audioTrack.play(); + } + } + + @Override + protected void onStopped() { + if (audioTrack.isInitialized()) { + audioTrack.pause(); + } + } + + @Override + protected boolean isEnded() { + // We've exhausted the input stream, and the AudioTrack has either played all of the data + // submitted, or has been fed insufficient data to begin playback. + return inputStreamEnded && (!audioTrack.hasPendingData() + || !audioTrack.hasEnoughDataToBeginPlayback()); + } + + @Override + protected boolean isReady() { + return audioTrack.hasPendingData() || (format != null && sourceState != SOURCE_STATE_NOT_READY); + } + + @Override + protected long getCurrentPositionUs() { + long audioTrackCurrentPositionUs = audioTrack.getCurrentPositionUs(isEnded()); + if (audioTrackCurrentPositionUs != AudioTrack.CURRENT_POSITION_NOT_SET) { + // Make sure we don't ever report time moving backwards. + currentPositionUs = Math.max(currentPositionUs, audioTrackCurrentPositionUs); + } + return currentPositionUs; + } + + @Override + protected long getDurationUs() { + return source.getTrackInfo(trackIndex).durationUs; + } + + @Override + protected long getBufferedPositionUs() { + long sourceBufferedPosition = source.getBufferedPositionUs(); + return sourceBufferedPosition == UNKNOWN_TIME_US || sourceBufferedPosition == END_OF_TRACK_US + ? sourceBufferedPosition : Math.max(sourceBufferedPosition, getCurrentPositionUs()); + } + + @Override + protected void onDisabled() { + audioSessionId = AudioTrack.SESSION_ID_NOT_SET; + shouldReadInputBuffer = true; + audioTrack.reset(); + } + + @Override + protected void seekTo(long positionUs) throws ExoPlaybackException { + source.seekToUs(positionUs); + sourceState = SOURCE_STATE_NOT_READY; + inputStreamEnded = false; + shouldReadInputBuffer = true; + + // TODO: Try and re-use the same AudioTrack instance once [Internal: b/7941810] is fixed. + audioTrack.reset(); + currentPositionUs = Long.MIN_VALUE; + } + + @Override + public void handleMessage(int messageType, Object message) throws ExoPlaybackException { + if (messageType == MSG_SET_VOLUME) { + audioTrack.setVolume((Float) message); + } else { + super.handleMessage(messageType, message); + } + } + + private void notifyAudioTrackInitializationError(final AudioTrack.InitializationException e) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackInitializationError(e); + } + }); + } + } + +}