From 978a4d857a436788249b0c439929109d09d9ddb8 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:02:29 +0000 Subject: [PATCH 1/5] Handle getting the audio track's position before the first AC-3 buffer. ac3Bitrate is set only after the first buffer is handled, which meant that getting the playback position would cause a divide by zero before then. When playing back AC-3 content, the ac3Bitrate will always be set after the first buffer is handled, so return a 0 position if it is not set. --- .../java/com/google/android/exoplayer/audio/AudioTrack.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 405f3e9c6b..2b109e79d3 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -641,7 +641,8 @@ public final class AudioTrack { private long bytesToFrames(long byteCount) { if (isAc3) { - return byteCount * 8 * sampleRate / (1000 * ac3Bitrate); + return + ac3Bitrate == UNKNOWN_AC3_BITRATE ? 0L : byteCount * 8 * sampleRate / (1000 * ac3Bitrate); } else { return byteCount / frameSize; } From 595147de9b4c0cf6ec1d784597457cba1af97507 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:03:10 +0000 Subject: [PATCH 2/5] Enforce sliding window of available segments for DASH DVB. --- .../exoplayer/dash/DashChunkSource.java | 48 ++++++++++++------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java index 8f0dc8b71c..a7bc566889 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/DashChunkSource.java @@ -326,13 +326,30 @@ public class DashChunkSource implements ChunkSource { return; } - int lastSegmentNum = segmentIndex.getLastSegmentNum(); - boolean indexUnbounded = lastSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + // TODO: Use UtcTimingElement where possible. + long nowUs = System.currentTimeMillis() * 1000; + + int firstAvailableSegmentNum = segmentIndex.getFirstSegmentNum(); + int lastAvailableSegmentNum = segmentIndex.getLastSegmentNum(); + boolean indexUnbounded = lastAvailableSegmentNum == DashSegmentIndex.INDEX_UNBOUNDED; + if (indexUnbounded) { + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; + if (currentManifest.timeShiftBufferDepth != -1) { + long bufferDepthUs = currentManifest.timeShiftBufferDepth * 1000; + firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, + segmentIndex.getSegmentNum(liveEdgeTimestampUs - bufferDepthUs)); + } + // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // index of the last completed segment. + lastAvailableSegmentNum = segmentIndex.getSegmentNum(liveEdgeTimestampUs) - 1; + } int segmentNum; if (queue.isEmpty()) { if (currentManifest.dynamic) { - seekPositionUs = getLiveSeekPosition(indexUnbounded); + seekPositionUs = getLiveSeekPosition(nowUs, indexUnbounded); } segmentNum = segmentIndex.getSegmentNum(seekPositionUs); } else { @@ -340,20 +357,20 @@ public class DashChunkSource implements ChunkSource { - representationHolder.segmentNumShift; } - // TODO: For unbounded manifests, we need to enforce that we don't try and request chunks - // behind or in front of the live window. if (currentManifest.dynamic) { - if (segmentNum < segmentIndex.getFirstSegmentNum()) { + if (segmentNum < firstAvailableSegmentNum) { // This is before the first chunk in the current manifest. fatalError = new BehindLiveWindowException(); return; - } else if (!indexUnbounded && segmentNum > lastSegmentNum) { - // This is beyond the last chunk in the current manifest. - finishedCurrentManifest = true; + } else if (segmentNum > lastAvailableSegmentNum) { + // This chunk is beyond the last chunk in the current manifest. If the index is bounded + // we'll need to refresh it. If it's unbounded we just need to wait for a while before + // attempting to load the chunk. + finishedCurrentManifest = !indexUnbounded; return; - } else if (!indexUnbounded && segmentNum == lastSegmentNum) { - // This is the last chunk in the current manifest. Mark the manifest as being finished, - // but continue to return the final chunk. + } else if (!indexUnbounded && segmentNum == lastAvailableSegmentNum) { + // This is the last chunk in a dynamic bounded manifest. We'll need to refresh the manifest + // to obtain the next chunk. finishedCurrentManifest = true; } } @@ -457,15 +474,14 @@ public class DashChunkSource implements ChunkSource { * For live playbacks, determines the seek position that snaps playback to be * {@link #liveEdgeLatencyUs} behind the live edge of the current manifest * + * @param nowUs An estimate of the current server time, in microseconds. * @param indexUnbounded True if the segment index for this source is unbounded. False otherwise. * @return The seek position in microseconds. */ - private long getLiveSeekPosition(boolean indexUnbounded) { + private long getLiveSeekPosition(long nowUs, boolean indexUnbounded) { long liveEdgeTimestampUs; if (indexUnbounded) { - // TODO: Use UtcTimingElement where possible. - long nowMs = System.currentTimeMillis(); - liveEdgeTimestampUs = (nowMs - currentManifest.availabilityStartTime) * 1000; + liveEdgeTimestampUs = nowUs - currentManifest.availabilityStartTime * 1000; } else { liveEdgeTimestampUs = Long.MIN_VALUE; for (RepresentationHolder representationHolder : representationHolders.values()) { From 11eb1c222b6fd5cd49d1013a3f8ac290d4d467b2 Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:04:38 +0000 Subject: [PATCH 3/5] Identify AC-3 tracks by codecs="ac-3", not the MIME type. --- .../demo/full/player/DashRendererBuilder.java | 9 ++++--- .../Ac3PassthroughAudioTrackRenderer.java | 2 +- .../android/exoplayer/chunk/Format.java | 25 ++++++++++++++++++- .../MediaPresentationDescriptionParser.java | 9 ++++--- 4 files changed, 35 insertions(+), 10 deletions(-) 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 bbc9e868e1..95f27f9545 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 @@ -81,6 +81,9 @@ public class DashRendererBuilder implements RendererBuilder, private static final int SECURITY_LEVEL_1 = 1; private static final int SECURITY_LEVEL_3 = 3; + private static final String AC_3_CODEC = "ac-3"; + private static final String E_AC_3_CODEC = "ec-3"; + private final String userAgent; private final String url; private final String contentId; @@ -225,15 +228,13 @@ 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); + haveAc3Tracks |= AC_3_CODEC.equals(format.codecs) || E_AC_3_CODEC.equals(format.codecs); } // 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)) { + if (!AC_3_CODEC.equals(format.codecs) && !E_AC_3_CODEC.equals(format.codecs)) { audioTrackNameList.remove(i); audioChunkSourceList.remove(i); } diff --git a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java index bfbe3b56d3..8b5b499878 100644 --- a/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/Ac3PassthroughAudioTrackRenderer.java @@ -136,7 +136,7 @@ public final class Ac3PassthroughAudioTrackRenderer extends TrackRenderer { } private static boolean handlesMimeType(String mimeType) { - return MimeTypes.AUDIO_AC3.equals(mimeType) || MimeTypes.AUDIO_EC3.equals(mimeType); + return MimeTypes.AUDIO_MP4.equals(mimeType); } @Override diff --git a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java index b2948c06a9..2810dc4ff5 100644 --- a/library/src/main/java/com/google/android/exoplayer/chunk/Format.java +++ b/library/src/main/java/com/google/android/exoplayer/chunk/Format.java @@ -46,6 +46,11 @@ public class Format { */ public final String mimeType; + /** + * The codecs used to decode the format, or {@code null} if they are not specified. + */ + public final String codecs; + /** * The width of the video in pixels, or -1 for non-video formats. */ @@ -98,7 +103,7 @@ public class Format { */ public Format(String id, String mimeType, int width, int height, int numChannels, int audioSamplingRate, int bitrate) { - this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, null); + this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, null, null); } /** @@ -113,6 +118,23 @@ public class Format { */ public Format(String id, String mimeType, int width, int height, int numChannels, int audioSamplingRate, int bitrate, String language) { + this(id, mimeType, width, height, numChannels, audioSamplingRate, bitrate, language, null); + } + + + /** + * @param id The format identifier. + * @param mimeType The format mime type. + * @param width The width of the video in pixels, or -1 for non-video formats. + * @param height The height of the video in pixels, or -1 for non-video formats. + * @param numChannels The number of audio channels, or -1 for non-audio formats. + * @param audioSamplingRate The audio sampling rate in Hz, or -1 for non-audio formats. + * @param bitrate The average bandwidth of the format in bits per second. + * @param language The language of the format. + * @param codecs The codecs used to decode the format. + */ + public Format(String id, String mimeType, int width, int height, int numChannels, + int audioSamplingRate, int bitrate, String language, String codecs) { this.id = Assertions.checkNotNull(id); this.mimeType = mimeType; this.width = width; @@ -121,6 +143,7 @@ public class Format { this.audioSamplingRate = audioSamplingRate; this.bitrate = bitrate; this.language = language; + this.codecs = codecs; this.bandwidth = bitrate / 8; } diff --git a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java index 4aa624cca1..edc73e2284 100644 --- a/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java +++ b/library/src/main/java/com/google/android/exoplayer/dash/mpd/MediaPresentationDescriptionParser.java @@ -283,6 +283,7 @@ public class MediaPresentationDescriptionParser extends DefaultHandler int width = parseInt(xpp, "width"); int height = parseInt(xpp, "height"); mimeType = parseString(xpp, "mimeType", mimeType); + String codecs = parseString(xpp, "codecs", null); int numChannels = -1; do { @@ -302,15 +303,15 @@ public class MediaPresentationDescriptionParser extends DefaultHandler } while (!isEndTag(xpp, "Representation")); Format format = buildFormat(id, mimeType, width, height, numChannels, audioSamplingRate, - bandwidth, language); + bandwidth, language, codecs); return buildRepresentation(periodStartMs, periodDurationMs, contentId, -1, format, segmentBase); } protected Format buildFormat(String id, String mimeType, int width, int height, int numChannels, - int audioSamplingRate, int bandwidth, String language) { - return new Format(id, mimeType, width, height, numChannels, audioSamplingRate, - bandwidth, language); + int audioSamplingRate, int bandwidth, String language, String codecs) { + return new Format(id, mimeType, width, height, numChannels, audioSamplingRate, bandwidth, + language, codecs); } protected Representation buildRepresentation(long periodStartMs, long periodDurationMs, From 57faa497561488cd81d485fb13849f0c9348c90d Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:05:06 +0000 Subject: [PATCH 4/5] Fix crash running ExoPlayer demo on JB. My bad! - We can't refer to UnsupportedSchemeException outside of the V18 compat inner classes. - There were also a few missing return; calls. --- .../demo/full/player/DashRendererBuilder.java | 26 ++++++++++--------- .../SmoothStreamingRendererBuilder.java | 20 +++++++------- 2 files changed, 25 insertions(+), 21 deletions(-) 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 95f27f9545..4c5e0f7d4f 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 @@ -163,12 +163,8 @@ public class DashRendererBuilder implements RendererBuilder, // HD streams require L1 security. filterHdContent = videoAdaptationSet != null && videoAdaptationSet.hasContentProtection() && !drmSessionManagerData.second; - } catch (UnsupportedSchemeException e) { - callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e)); - } catch (Exception e) { - callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e)); + } catch (UnsupportedDrmException e) { + callback.onRenderersError(e); return; } } @@ -328,12 +324,18 @@ public class DashRendererBuilder implements RendererBuilder, private static class V18Compat { public static Pair getDrmSessionManagerData(DemoPlayer player, - MediaDrmCallback drmCallback) throws UnsupportedSchemeException { - StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( - DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, null, - player.getMainHandler(), player); - return Pair.create((DrmSessionManager) streamingDrmSessionManager, - getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); + MediaDrmCallback drmCallback) throws UnsupportedDrmException { + try { + StreamingDrmSessionManager streamingDrmSessionManager = new StreamingDrmSessionManager( + DemoUtil.WIDEVINE_UUID, player.getPlaybackLooper(), drmCallback, null, + player.getMainHandler(), player); + return Pair.create((DrmSessionManager) streamingDrmSessionManager, + getWidevineSecurityLevel(streamingDrmSessionManager) == SECURITY_LEVEL_1); + } catch (UnsupportedSchemeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } catch (Exception e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e); + } } private static int getWidevineSecurityLevel(StreamingDrmSessionManager sessionManager) { diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java index e02ebe5ca8..6225f42c58 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/full/player/SmoothStreamingRendererBuilder.java @@ -118,12 +118,8 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, try { drmSessionManager = V18Compat.getDrmSessionManager(manifest.protectionElement.uuid, player, drmCallback); - } catch (UnsupportedSchemeException e) { - callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME, e)); - } catch (Exception e) { - callback.onRenderersError( - new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e)); + } catch (UnsupportedDrmException e) { + callback.onRenderersError(e); return; } } @@ -259,9 +255,15 @@ public class SmoothStreamingRendererBuilder implements RendererBuilder, private static class V18Compat { public static DrmSessionManager getDrmSessionManager(UUID uuid, DemoPlayer player, - MediaDrmCallback drmCallback) throws UnsupportedSchemeException { - return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null, - player.getMainHandler(), player); + MediaDrmCallback drmCallback) throws UnsupportedDrmException { + try { + return new StreamingDrmSessionManager(uuid, player.getPlaybackLooper(), drmCallback, null, + player.getMainHandler(), player); + } catch (UnsupportedSchemeException e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME); + } catch (Exception e) { + throw new UnsupportedDrmException(UnsupportedDrmException.REASON_UNKNOWN, e); + } } } From 57068a6406974060394e528be9bd28d8c125072e Mon Sep 17 00:00:00 2001 From: Oliver Woodman Date: Mon, 15 Dec 2014 15:06:05 +0000 Subject: [PATCH 5/5] Clear subtitle when text disabled. --- .../google/android/exoplayer/demo/full/player/DemoPlayer.java | 3 +++ 1 file changed, 3 insertions(+) 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 97ce5f5506..a426295b3c 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 @@ -246,6 +246,9 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi pushSurfaceAndVideoTrack(false); } else { pushTrackSelection(type, true); + if (type == TYPE_TEXT && index == DISABLED_TRACK && textListener != null) { + textListener.onText(null); + } } }