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..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 @@ -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; @@ -160,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; } } @@ -225,15 +224,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); } @@ -327,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); + } } } 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/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; } 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/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()) { 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,