From e3f7dc18f93d29bab52381e3fba978a79d3d07e3 Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Tue, 26 Jun 2018 08:00:43 +0200 Subject: [PATCH 01/23] Remove useless code --- .../src/main/java/ch/srg/mediaplayer/SRGMediaPlayerView.java | 5 ----- 1 file changed, 5 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerView.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerView.java index a3a5f66..eb17151 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerView.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerView.java @@ -286,11 +286,6 @@ protected void onLayout(boolean changed, int left, int top, int right, int botto } } - @Override - protected void onAttachedToWindow() { - super.onAttachedToWindow(); - } - @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { final int specWidth = MeasureSpec.getSize(widthMeasureSpec); From ba3740762d519c6c2cbb0bc72173251a4df4b0a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 16 Jul 2018 09:45:14 +0200 Subject: [PATCH 02/23] Add DRM, hardcoded licence for quick tests, WIP --- .../mediaplayer/SRGMediaPlayerController.java | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 4d01247..a410d33 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -34,6 +34,12 @@ import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioCapabilities; import com.google.android.exoplayer2.audio.AudioCapabilitiesReceiver; +import com.google.android.exoplayer2.drm.DefaultDrmSessionManager; +import com.google.android.exoplayer2.drm.DrmSessionManager; +import com.google.android.exoplayer2.drm.FrameworkMediaCrypto; +import com.google.android.exoplayer2.drm.FrameworkMediaDrm; +import com.google.android.exoplayer2.drm.HttpMediaDrmCallback; +import com.google.android.exoplayer2.drm.UnsupportedDrmException; import com.google.android.exoplayer2.extractor.DefaultExtractorsFactory; import com.google.android.exoplayer2.source.ExtractorMediaSource; import com.google.android.exoplayer2.source.MediaSource; @@ -55,6 +61,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -450,6 +457,9 @@ public interface Listener { @Nullable private AkamaiMediaAnalyticsConfiguration akamaiMediaAnalyticsConfiguration; + // FIXME : why userAgent letterbox is set here? + private static final String userAgent = "curl/Letterbox_2.0"; // temporarily using curl/ user agent to force subtitles with Akamai beta + private static final String LICENCE_URL = "https://rng.stage.ott.irdeto.com/licenseServer/widevine/v1/SRG/license?contentId=srg-content"; /** * Create a new SRGMediaPlayerController with the current context, a mediaPlayerDataProvider, and a TAG @@ -475,15 +485,29 @@ public SRGMediaPlayerController(Context context, String tag) { trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); eventLogger = new EventLogger(trackSelector); - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this.context, null, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); + DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; + //DRM is only supported at API level 18+ + if (Util.SDK_INT >= 18) { + try { + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(LICENCE_URL, new DefaultHttpDataSourceFactory(userAgent)); + drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, + FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), + drmCallback, null, mainHandler, eventLogger); + } catch (UnsupportedDrmException e) { + // TODO : Post DRMErrorEvent + // fatalError = e; + e.printStackTrace(); + } + } + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this.context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); exoPlayer.addListener(this); - exoPlayer.setVideoListener(this); - exoPlayer.setTextOutput(this); + exoPlayer.addVideoListener(this); + exoPlayer.addTextOutput(this); exoPlayer.setAudioDebugListener(eventLogger); exoPlayer.setVideoDebugListener(eventLogger); - exoPlayer.setMetadataOutput(eventLogger); + exoPlayer.addMetadataOutput(eventLogger); exoPlayerCurrentPlayWhenReady = exoPlayer.getPlayWhenReady(); audioFocusChangeListener = new OnAudioFocusChangeListener(new WeakReference<>(this)); @@ -868,7 +892,6 @@ private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMe sendMessage(MSG_PLAYER_PREPARING); this.currentMediaUri = videoUri; - String userAgent = "curl/Letterbox_2.0"; // temporarily using curl/ user agent to force subtitles with Akamai beta DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( userAgent, From ff8717b81b8c8e78e80df7570d84385638d101cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 16 Jul 2018 13:33:09 +0200 Subject: [PATCH 03/23] Restore exoplayer 2.5.1 methods --- .../java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index a410d33..ae7426b 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -503,11 +503,11 @@ public SRGMediaPlayerController(Context context, String tag) { exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); exoPlayer.addListener(this); - exoPlayer.addVideoListener(this); - exoPlayer.addTextOutput(this); + exoPlayer.setVideoListener(this); + exoPlayer.setTextOutput(this); exoPlayer.setAudioDebugListener(eventLogger); exoPlayer.setVideoDebugListener(eventLogger); - exoPlayer.addMetadataOutput(eventLogger); + exoPlayer.setMetadataOutput(eventLogger); exoPlayerCurrentPlayWhenReady = exoPlayer.getPlayWhenReady(); audioFocusChangeListener = new OnAudioFocusChangeListener(new WeakReference<>(this)); @@ -908,7 +908,7 @@ private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMe switch (streamType) { case STREAM_DASH: - mediaSource = new DashMediaSource(videoUri, dataSourceFactory, + mediaSource = new DashMediaSource(videoUri, new DefaultHttpDataSourceFactory(userAgent), new DefaultDashChunkSource.Factory(dataSourceFactory), mainHandler, eventLogger); break; case STREAM_HLS: From 96b622c510f3e16b25eb110cf5595e63296b2969 Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Tue, 17 Jul 2018 10:19:53 +0200 Subject: [PATCH 04/23] WIP: try custom DashChunkSource to circumvent track selection issue --- .../mediaplayer/DefaultDashChunkSource.java | 510 ++++++++++++++++++ .../mediaplayer/SRGMediaPlayerController.java | 3 +- 2 files changed, 511 insertions(+), 2 deletions(-) create mode 100644 srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java new file mode 100644 index 0000000..c7cd217 --- /dev/null +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java @@ -0,0 +1,510 @@ +/* + * Copyright (C) 2016 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 ch.srg.mediaplayer; + +import android.net.Uri; +import android.os.SystemClock; +import android.util.Log; + +import com.google.android.exoplayer2.C; +import com.google.android.exoplayer2.Format; +import com.google.android.exoplayer2.extractor.ChunkIndex; +import com.google.android.exoplayer2.extractor.Extractor; +import com.google.android.exoplayer2.extractor.SeekMap; +import com.google.android.exoplayer2.extractor.mkv.MatroskaExtractor; +import com.google.android.exoplayer2.extractor.mp4.FragmentedMp4Extractor; +import com.google.android.exoplayer2.extractor.rawcc.RawCcExtractor; +import com.google.android.exoplayer2.source.BehindLiveWindowException; +import com.google.android.exoplayer2.source.chunk.Chunk; +import com.google.android.exoplayer2.source.chunk.ChunkExtractorWrapper; +import com.google.android.exoplayer2.source.chunk.ChunkHolder; +import com.google.android.exoplayer2.source.chunk.ChunkedTrackBlacklistUtil; +import com.google.android.exoplayer2.source.chunk.ContainerMediaChunk; +import com.google.android.exoplayer2.source.chunk.InitializationChunk; +import com.google.android.exoplayer2.source.chunk.MediaChunk; +import com.google.android.exoplayer2.source.chunk.SingleSampleMediaChunk; +import com.google.android.exoplayer2.source.dash.DashChunkSource; +import com.google.android.exoplayer2.source.dash.DashSegmentIndex; +import com.google.android.exoplayer2.source.dash.DashWrappingSegmentIndex; +import com.google.android.exoplayer2.source.dash.manifest.AdaptationSet; +import com.google.android.exoplayer2.source.dash.manifest.DashManifest; +import com.google.android.exoplayer2.source.dash.manifest.RangedUri; +import com.google.android.exoplayer2.source.dash.manifest.Representation; +import com.google.android.exoplayer2.trackselection.TrackSelection; +import com.google.android.exoplayer2.upstream.DataSource; +import com.google.android.exoplayer2.upstream.DataSpec; +import com.google.android.exoplayer2.upstream.HttpDataSource.InvalidResponseCodeException; +import com.google.android.exoplayer2.upstream.LoaderErrorThrower; +import com.google.android.exoplayer2.util.MimeTypes; +import com.google.android.exoplayer2.util.Util; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * A default {@link DashChunkSource} implementation. + */ +public class DefaultDashChunkSource implements DashChunkSource { + + public static final class Factory implements DashChunkSource.Factory { + + private final DataSource.Factory dataSourceFactory; + private final int maxSegmentsPerLoad; + + public Factory(DataSource.Factory dataSourceFactory) { + this(dataSourceFactory, 1); + } + + public Factory(DataSource.Factory dataSourceFactory, int maxSegmentsPerLoad) { + this.dataSourceFactory = dataSourceFactory; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; + } + + @Override + public DashChunkSource createDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, long elapsedRealtimeOffsetMs, + boolean enableEventMessageTrack, boolean enableCea608Track) { + DataSource dataSource = dataSourceFactory.createDataSource(); + return new DefaultDashChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, + adaptationSetIndices, trackSelection, trackType, dataSource, elapsedRealtimeOffsetMs, + maxSegmentsPerLoad, enableEventMessageTrack, enableCea608Track); + } + + } + + private final LoaderErrorThrower manifestLoaderErrorThrower; + private final int[] adaptationSetIndices; + private final TrackSelection trackSelection; + private final int trackType; + private final RepresentationHolder[] representationHolders; + private final DataSource dataSource; + private final long elapsedRealtimeOffsetMs; + private final int maxSegmentsPerLoad; + + private DashManifest manifest; + private int periodIndex; + + private IOException fatalError; + private boolean missingLastSegment; + + /** + * @param manifestLoaderErrorThrower Throws errors affecting loading of manifests. + * @param manifest The initial manifest. + * @param periodIndex The index of the period in the manifest. + * @param adaptationSetIndices The indices of the adaptation sets in the period. + * @param trackSelection The track selection. + * @param trackType The type of the tracks in the selection. + * @param dataSource A {@link DataSource} suitable for loading the media data. + * @param elapsedRealtimeOffsetMs If known, an estimate of the instantaneous difference between + * server-side unix time and {@link SystemClock#elapsedRealtime()} in milliseconds, specified + * as the server's unix time minus the local elapsed time. If unknown, set to 0. + * @param maxSegmentsPerLoad The maximum number of segments to combine into a single request. + * Note that segments will only be combined if their {@link Uri}s are the same and if their + * data ranges are adjacent. + * @param enableEventMessageTrack Whether the chunks generated by the source may output an event + * message track. + * @param enableCea608Track Whether the chunks generated by the source may output a CEA-608 track. + */ + public DefaultDashChunkSource(LoaderErrorThrower manifestLoaderErrorThrower, + DashManifest manifest, int periodIndex, int[] adaptationSetIndices, + TrackSelection trackSelection, int trackType, DataSource dataSource, + long elapsedRealtimeOffsetMs, int maxSegmentsPerLoad, boolean enableEventMessageTrack, + boolean enableCea608Track) { + this.manifestLoaderErrorThrower = manifestLoaderErrorThrower; + this.manifest = manifest; + this.adaptationSetIndices = adaptationSetIndices; + this.trackSelection = trackSelection; + this.trackType = trackType; + this.dataSource = dataSource; + this.periodIndex = periodIndex; + this.elapsedRealtimeOffsetMs = elapsedRealtimeOffsetMs; + this.maxSegmentsPerLoad = maxSegmentsPerLoad; + + long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + List<Representation> representations = getRepresentations(); + representationHolders = new RepresentationHolder[trackSelection.length()]; + for (int i = 0; i < representationHolders.length; i++) { + Representation representation = representations.get(trackSelection.getIndexInTrackGroup(i)); + representationHolders[i] = new RepresentationHolder(periodDurationUs, representation, + enableEventMessageTrack, enableCea608Track); + } + } + + @Override + public void updateManifest(DashManifest newManifest, int newPeriodIndex) { + try { + manifest = newManifest; + periodIndex = newPeriodIndex; + long periodDurationUs = manifest.getPeriodDurationUs(periodIndex); + List<Representation> representations = getRepresentations(); + for (int i = 0; i < representationHolders.length; i++) { + int index = trackSelection.getIndexInTrackGroup(i); + if (index >= representations.size()) { + index = 0; + Log.e("DefaultDashChunkSource", "invalid track index " + index + " (" + representations.size() + ") "); + } + Representation representation = representations.get(index); + representationHolders[i].updateRepresentation(periodDurationUs, representation); + } + } catch (BehindLiveWindowException e) { + fatalError = e; + } + } + + @Override + public void maybeThrowError() throws IOException { + if (fatalError != null) { + throw fatalError; + } else { + manifestLoaderErrorThrower.maybeThrowError(); + } + } + + @Override + public int getPreferredQueueSize(long playbackPositionUs, List<? extends MediaChunk> queue) { + if (fatalError != null || trackSelection.length() < 2) { + return queue.size(); + } + return trackSelection.evaluateQueueSize(playbackPositionUs, queue); + } + + @Override + public final void getNextChunk(MediaChunk previous, long playbackPositionUs, ChunkHolder out) { + if (fatalError != null) { + return; + } + + long bufferedDurationUs = previous != null ? (previous.endTimeUs - playbackPositionUs) : 0; + trackSelection.updateSelectedTrack(bufferedDurationUs); + + RepresentationHolder representationHolder = + representationHolders[trackSelection.getSelectedIndex()]; + + if (representationHolder.extractorWrapper != null) { + Representation selectedRepresentation = representationHolder.representation; + RangedUri pendingInitializationUri = null; + RangedUri pendingIndexUri = null; + if (representationHolder.extractorWrapper.getSampleFormats() == null) { + pendingInitializationUri = selectedRepresentation.getInitializationUri(); + } + if (representationHolder.segmentIndex == null) { + pendingIndexUri = selectedRepresentation.getIndexUri(); + } + if (pendingInitializationUri != null || pendingIndexUri != null) { + // We have initialization and/or index requests to make. + out.chunk = newInitializationChunk(representationHolder, dataSource, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), pendingInitializationUri, pendingIndexUri); + return; + } + } + + long nowUs = getNowUnixTimeUs(); + int availableSegmentCount = representationHolder.getSegmentCount(); + if (availableSegmentCount == 0) { + // The index doesn't define any segments. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + + int firstAvailableSegmentNum = representationHolder.getFirstSegmentNum(); + int lastAvailableSegmentNum; + if (availableSegmentCount == DashSegmentIndex.INDEX_UNBOUNDED) { + // The index is itself unbounded. We need to use the current time to calculate the range of + // available segments. + long liveEdgeTimeUs = nowUs - manifest.availabilityStartTime * 1000; + long periodStartUs = manifest.getPeriod(periodIndex).startMs * 1000; + long liveEdgeTimeInPeriodUs = liveEdgeTimeUs - periodStartUs; + if (manifest.timeShiftBufferDepth != C.TIME_UNSET) { + long bufferDepthUs = manifest.timeShiftBufferDepth * 1000; + firstAvailableSegmentNum = Math.max(firstAvailableSegmentNum, + representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs - bufferDepthUs)); + } + // getSegmentNum(liveEdgeTimestampUs) will not be completed yet, so subtract one to get the + // index of the last completed segment. + lastAvailableSegmentNum = representationHolder.getSegmentNum(liveEdgeTimeInPeriodUs) - 1; + } else { + lastAvailableSegmentNum = firstAvailableSegmentNum + availableSegmentCount - 1; + } + + int segmentNum; + if (previous == null) { + segmentNum = Util.constrainValue(representationHolder.getSegmentNum(playbackPositionUs), + firstAvailableSegmentNum, lastAvailableSegmentNum); + } else { + segmentNum = previous.getNextChunkIndex(); + if (segmentNum < firstAvailableSegmentNum) { + // This is before the first chunk in the current manifest. + fatalError = new BehindLiveWindowException(); + return; + } + } + + if (segmentNum > lastAvailableSegmentNum + || (missingLastSegment && segmentNum >= lastAvailableSegmentNum)) { + // This is beyond the last chunk in the current manifest. + out.endOfStream = !manifest.dynamic || (periodIndex < manifest.getPeriodCount() - 1); + return; + } + + int maxSegmentCount = Math.min(maxSegmentsPerLoad, lastAvailableSegmentNum - segmentNum + 1); + out.chunk = newMediaChunk(representationHolder, dataSource, trackType, + trackSelection.getSelectedFormat(), trackSelection.getSelectionReason(), + trackSelection.getSelectionData(), segmentNum, maxSegmentCount); + } + + @Override + public void onChunkLoadCompleted(Chunk chunk) { + if (chunk instanceof InitializationChunk) { + InitializationChunk initializationChunk = (InitializationChunk) chunk; + RepresentationHolder representationHolder = + representationHolders[trackSelection.indexOf(initializationChunk.trackFormat)]; + // The null check avoids overwriting an index obtained from the manifest with one obtained + // from the stream. If the manifest defines an index then the stream shouldn't, but in cases + // where it does we should ignore it. + if (representationHolder.segmentIndex == null) { + SeekMap seekMap = representationHolder.extractorWrapper.getSeekMap(); + if (seekMap != null) { + representationHolder.segmentIndex = new DashWrappingSegmentIndex((ChunkIndex) seekMap); + } + } + } + } + + @Override + public boolean onChunkLoadError(Chunk chunk, boolean cancelable, Exception e) { + if (!cancelable) { + return false; + } + // Workaround for missing segment at the end of the period + if (!manifest.dynamic && chunk instanceof MediaChunk + && e instanceof InvalidResponseCodeException + && ((InvalidResponseCodeException) e).responseCode == 404) { + RepresentationHolder representationHolder = + representationHolders[trackSelection.indexOf(chunk.trackFormat)]; + int segmentCount = representationHolder.getSegmentCount(); + if (segmentCount != DashSegmentIndex.INDEX_UNBOUNDED && segmentCount != 0) { + int lastAvailableSegmentNum = representationHolder.getFirstSegmentNum() + segmentCount - 1; + if (((MediaChunk) chunk).getNextChunkIndex() > lastAvailableSegmentNum) { + missingLastSegment = true; + return true; + } + } + } + // Blacklist if appropriate. + return ChunkedTrackBlacklistUtil.maybeBlacklistTrack(trackSelection, + trackSelection.indexOf(chunk.trackFormat), e); + } + + // Private methods. + + private ArrayList<Representation> getRepresentations() { + List<AdaptationSet> manifestAdapationSets = manifest.getPeriod(periodIndex).adaptationSets; + ArrayList<Representation> representations = new ArrayList<>(); + for (int adaptationSetIndex : adaptationSetIndices) { + representations.addAll(manifestAdapationSets.get(adaptationSetIndex).representations); + } + return representations; + } + + private long getNowUnixTimeUs() { + if (elapsedRealtimeOffsetMs != 0) { + return (SystemClock.elapsedRealtime() + elapsedRealtimeOffsetMs) * 1000; + } else { + return System.currentTimeMillis() * 1000; + } + } + + private static Chunk newInitializationChunk(RepresentationHolder representationHolder, + DataSource dataSource, Format trackFormat, int trackSelectionReason, + Object trackSelectionData, RangedUri initializationUri, RangedUri indexUri) { + RangedUri requestUri; + String baseUrl = representationHolder.representation.baseUrl; + if (initializationUri != null) { + // It's common for initialization and index data to be stored adjacently. Attempt to merge + // the two requests together to request both at once. + requestUri = initializationUri.attemptMerge(indexUri, baseUrl); + if (requestUri == null) { + requestUri = initializationUri; + } + } else { + requestUri = indexUri; + } + DataSpec dataSpec = new DataSpec(requestUri.resolveUri(baseUrl), requestUri.start, + requestUri.length, representationHolder.representation.getCacheKey()); + return new InitializationChunk(dataSource, dataSpec, trackFormat, + trackSelectionReason, trackSelectionData, representationHolder.extractorWrapper); + } + + private static Chunk newMediaChunk(RepresentationHolder representationHolder, + DataSource dataSource, int trackType, Format trackFormat, int trackSelectionReason, + Object trackSelectionData, int firstSegmentNum, int maxSegmentCount) { + Representation representation = representationHolder.representation; + long startTimeUs = representationHolder.getSegmentStartTimeUs(firstSegmentNum); + RangedUri segmentUri = representationHolder.getSegmentUrl(firstSegmentNum); + String baseUrl = representation.baseUrl; + if (representationHolder.extractorWrapper == null) { + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); + return new SingleSampleMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, trackType, trackFormat); + } else { + int segmentCount = 1; + for (int i = 1; i < maxSegmentCount; i++) { + RangedUri nextSegmentUri = representationHolder.getSegmentUrl(firstSegmentNum + i); + RangedUri mergedSegmentUri = segmentUri.attemptMerge(nextSegmentUri, baseUrl); + if (mergedSegmentUri == null) { + // Unable to merge segment fetches because the URIs do not merge. + break; + } + segmentUri = mergedSegmentUri; + segmentCount++; + } + long endTimeUs = representationHolder.getSegmentEndTimeUs(firstSegmentNum + segmentCount - 1); + DataSpec dataSpec = new DataSpec(segmentUri.resolveUri(baseUrl), + segmentUri.start, segmentUri.length, representation.getCacheKey()); + long sampleOffsetUs = -representation.presentationTimeOffsetUs; + return new ContainerMediaChunk(dataSource, dataSpec, trackFormat, trackSelectionReason, + trackSelectionData, startTimeUs, endTimeUs, firstSegmentNum, segmentCount, + sampleOffsetUs, representationHolder.extractorWrapper); + } + } + + // Protected classes. + + protected static final class RepresentationHolder { + + public final ChunkExtractorWrapper extractorWrapper; + + public Representation representation; + public DashSegmentIndex segmentIndex; + + private long periodDurationUs; + private int segmentNumShift; + + public RepresentationHolder(long periodDurationUs, Representation representation, + boolean enableEventMessageTrack, boolean enableCea608Track) { + this.periodDurationUs = periodDurationUs; + this.representation = representation; + String containerMimeType = representation.format.containerMimeType; + if (mimeTypeIsRawText(containerMimeType)) { + extractorWrapper = null; + } else { + Extractor extractor; + if (MimeTypes.APPLICATION_RAWCC.equals(containerMimeType)) { + extractor = new RawCcExtractor(representation.format); + } else if (mimeTypeIsWebm(containerMimeType)) { + extractor = new MatroskaExtractor(MatroskaExtractor.FLAG_DISABLE_SEEK_FOR_CUES); + } else { + int flags = 0; + if (enableEventMessageTrack) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_EMSG_TRACK; + } + if (enableCea608Track) { + flags |= FragmentedMp4Extractor.FLAG_ENABLE_CEA608_TRACK; + } + extractor = new FragmentedMp4Extractor(flags); + } + // Prefer drmInitData obtained from the manifest over drmInitData obtained from the stream, + // as per DASH IF Interoperability Recommendations V3.0, 7.5.3. + extractorWrapper = new ChunkExtractorWrapper(extractor, representation.format); + } + segmentIndex = representation.getIndex(); + } + + public void updateRepresentation(long newPeriodDurationUs, Representation newRepresentation) + throws BehindLiveWindowException { + DashSegmentIndex oldIndex = representation.getIndex(); + DashSegmentIndex newIndex = newRepresentation.getIndex(); + + periodDurationUs = newPeriodDurationUs; + representation = newRepresentation; + if (oldIndex == null) { + // Segment numbers cannot shift if the index isn't defined by the manifest. + return; + } + + segmentIndex = newIndex; + if (!oldIndex.isExplicit()) { + // Segment numbers cannot shift if the index isn't explicit. + return; + } + + int oldIndexSegmentCount = oldIndex.getSegmentCount(periodDurationUs); + if (oldIndexSegmentCount == 0) { + // Segment numbers cannot shift if the old index was empty. + return; + } + + int oldIndexLastSegmentNum = oldIndex.getFirstSegmentNum() + oldIndexSegmentCount - 1; + long oldIndexEndTimeUs = oldIndex.getTimeUs(oldIndexLastSegmentNum) + + oldIndex.getDurationUs(oldIndexLastSegmentNum, periodDurationUs); + int newIndexFirstSegmentNum = newIndex.getFirstSegmentNum(); + long newIndexStartTimeUs = newIndex.getTimeUs(newIndexFirstSegmentNum); + if (oldIndexEndTimeUs == newIndexStartTimeUs) { + // The new index continues where the old one ended, with no overlap. + segmentNumShift += oldIndexLastSegmentNum + 1 - newIndexFirstSegmentNum; + } else if (oldIndexEndTimeUs < newIndexStartTimeUs) { + // There's a gap between the old index and the new one which means we've slipped behind the + // live window and can't proceed. + throw new BehindLiveWindowException(); + } else { + // The new index overlaps with the old one. + segmentNumShift += oldIndex.getSegmentNum(newIndexStartTimeUs, periodDurationUs) + - newIndexFirstSegmentNum; + } + } + + public int getFirstSegmentNum() { + return segmentIndex.getFirstSegmentNum() + segmentNumShift; + } + + public int getSegmentCount() { + return segmentIndex.getSegmentCount(periodDurationUs); + } + + public long getSegmentStartTimeUs(int segmentNum) { + return segmentIndex.getTimeUs(segmentNum - segmentNumShift); + } + + public long getSegmentEndTimeUs(int segmentNum) { + return getSegmentStartTimeUs(segmentNum) + + segmentIndex.getDurationUs(segmentNum - segmentNumShift, periodDurationUs); + } + + public int getSegmentNum(long positionUs) { + return segmentIndex.getSegmentNum(positionUs, periodDurationUs) + segmentNumShift; + } + + public RangedUri getSegmentUrl(int segmentNum) { + return segmentIndex.getSegmentUrl(segmentNum - segmentNumShift); + } + + private static boolean mimeTypeIsWebm(String mimeType) { + return mimeType.startsWith(MimeTypes.VIDEO_WEBM) || mimeType.startsWith(MimeTypes.AUDIO_WEBM) + || mimeType.startsWith(MimeTypes.APPLICATION_WEBM); + } + + private static boolean mimeTypeIsRawText(String mimeType) { + return MimeTypes.isText(mimeType) || MimeTypes.APPLICATION_TTML.equals(mimeType); + } + + } + +} diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index ae7426b..1edc922 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; @@ -489,7 +488,7 @@ public SRGMediaPlayerController(Context context, String tag) { //DRM is only supported at API level 18+ if (Util.SDK_INT >= 18) { try { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(LICENCE_URL, new DefaultHttpDataSourceFactory(userAgent)); + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(LICENCE_URL, new DefaultHttpDataSourceFactory("Mozilla/5.0 (Linux; Android 7.1.1; F5321 Build/34.3.A.0.238) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36")); drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), drmCallback, null, mainHandler, eventLogger); From 2fa4454703b9d8da164f073260c1f223e6f0c927 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:08:15 +0200 Subject: [PATCH 05/23] Manage playback drm error --- .../SRGDrmMediaPlayerException.java | 20 ++++++++++++++++ .../mediaplayer/SRGMediaPlayerController.java | 23 +++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGDrmMediaPlayerException.java diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGDrmMediaPlayerException.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGDrmMediaPlayerException.java new file mode 100644 index 0000000..26c52f1 --- /dev/null +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGDrmMediaPlayerException.java @@ -0,0 +1,20 @@ +package ch.srg.mediaplayer; + +/** + * Copyright (c) SRG SSR. All rights reserved. + * <p> + * License information is available from the LICENSE file. + */ +public class SRGDrmMediaPlayerException extends SRGMediaPlayerException { + public SRGDrmMediaPlayerException(String detailMessage) { + super(detailMessage); + } + + public SRGDrmMediaPlayerException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public SRGDrmMediaPlayerException(Throwable throwable) { + super(throwable); + } +} diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 1edc922..26c808f 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -83,6 +83,7 @@ @SuppressWarnings({"unused", "unchecked", "UnusedReturnValue", "WeakerAccess", "PointlessBitwiseExpression"}) public class SRGMediaPlayerController implements Handler.Callback, Player.EventListener, + DefaultDrmSessionManager.EventListener, SimpleExoPlayer.VideoListener, AudioCapabilitiesReceiver.Listener, TextRenderer.Output { @@ -2044,6 +2045,28 @@ public void onCues(List<Cue> cues) { sendMessage(MSG_PLAYER_SUBTITLE_CUES, cues); } + @Override + public void onDrmKeysLoaded() { + eventLogger.onDrmKeysLoaded(); + } + + @Override + public void onDrmSessionManagerError(Exception e) { + eventLogger.onDrmSessionManagerError(e); + postFatalErrorInternal(new SRGDrmMediaPlayerException(e)); + } + + @Override + public void onDrmKeysRestored() { + eventLogger.onDrmKeysRestored(); + } + + @Override + public void onDrmKeysRemoved() { + eventLogger.onDrmKeysRemoved(); + } + + /** * Provide Akamai QOS Configuration. * From 9c0e2304e36aefecd64ff027e6680246c6c7a50f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:08:43 +0200 Subject: [PATCH 06/23] Add DrmConfig --- .../java/ch/srg/mediaplayer/DrmConfig.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 srgmediaplayer/src/main/java/ch/srg/mediaplayer/DrmConfig.java diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DrmConfig.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DrmConfig.java new file mode 100644 index 0000000..b03107f --- /dev/null +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DrmConfig.java @@ -0,0 +1,26 @@ +package ch.srg.mediaplayer; + +import java.util.UUID; + +/** + * Copyright (c) SRG SSR. All rights reserved. + * <p> + * License information is available from the LICENSE file. + */ +public class DrmConfig { + private String licenceUrl; + private UUID drmType; + + public DrmConfig(String licenceUrl, UUID drmType) { + this.licenceUrl = licenceUrl; + this.drmType = drmType; + } + + public String getLicenceUrl() { + return licenceUrl; + } + + public UUID getDrmType() { + return drmType; + } +} From b5b881d71dd88dadf39484bbe733102faf629f4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:10:49 +0200 Subject: [PATCH 07/23] Configure DRM Session manager from DrmConfig --- .../mediaplayer/SRGMediaPlayerController.java | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 26c808f..204a432 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -275,7 +275,9 @@ public enum Type { /** * The Segment list has changed. */ - SEGMENT_LIST_CHANGE + SEGMENT_LIST_CHANGE, + + UNSUPPORTED_DRM } public final Type type; @@ -459,7 +461,8 @@ public interface Listener { private AkamaiMediaAnalyticsConfiguration akamaiMediaAnalyticsConfiguration; // FIXME : why userAgent letterbox is set here? private static final String userAgent = "curl/Letterbox_2.0"; // temporarily using curl/ user agent to force subtitles with Akamai beta - private static final String LICENCE_URL = "https://rng.stage.ott.irdeto.com/licenseServer/widevine/v1/SRG/license?contentId=srg-content"; + @Nullable + private DrmConfig drmConfig; /** * Create a new SRGMediaPlayerController with the current context, a mediaPlayerDataProvider, and a TAG @@ -468,7 +471,7 @@ public interface Listener { * @param context context * @param tag tag to identify this controller */ - public SRGMediaPlayerController(Context context, String tag) { + public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmConfig) { this.context = context; this.mainHandler = new Handler(Looper.getMainLooper(), this); this.tag = tag; @@ -486,17 +489,19 @@ public SRGMediaPlayerController(Context context, String tag) { trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); eventLogger = new EventLogger(trackSelector); DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; - //DRM is only supported at API level 18+ - if (Util.SDK_INT >= 18) { + //&& Util.SDK_INT >= 18 + if (drmConfig != null) { try { - HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(LICENCE_URL, new DefaultHttpDataSourceFactory("Mozilla/5.0 (Linux; Android 7.1.1; F5321 Build/34.3.A.0.238) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/64.0.3282.137 Mobile Safari/537.36")); - drmSessionManager = new DefaultDrmSessionManager<>(C.WIDEVINE_UUID, - FrameworkMediaDrm.newInstance(C.WIDEVINE_UUID), - drmCallback, null, mainHandler, eventLogger); + UUID drmType = drmConfig.getDrmType(); + HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmConfig.getLicenceUrl(), new DefaultHttpDataSourceFactory(userAgent)); + drmSessionManager = new DefaultDrmSessionManager<>(drmType, + FrameworkMediaDrm.newInstance(drmType), + drmCallback, null, mainHandler, this); + setViewType(ViewType.TYPE_SURFACEVIEW); } catch (UnsupportedDrmException e) { - // TODO : Post DRMErrorEvent - // fatalError = e; - e.printStackTrace(); + Event event = Event.buildErrorEvent(this, e); + fatalError = event.exception; + postEventInternal(event); } } DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this.context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); From 2bbd245451e6d814aea9e62e3a2b45c07f32198b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:22:37 +0200 Subject: [PATCH 08/23] Error handling when drm not supported at MediaPlayer creation --- .../srg/mediaplayer/SRGMediaPlayerController.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 204a432..430f5dd 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -60,7 +60,6 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSourceFactory; -import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -71,6 +70,7 @@ import java.util.HashSet; import java.util.List; import java.util.Set; +import java.util.UUID; import java.util.WeakHashMap; import ch.srg.mediaplayer.segment.model.Segment; @@ -489,7 +489,7 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig trackSelector = new DefaultTrackSelector(videoTrackSelectionFactory); eventLogger = new EventLogger(trackSelector); DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; - //&& Util.SDK_INT >= 18 + UnsupportedDrmException unsupportedDrm = null; if (drmConfig != null) { try { UUID drmType = drmConfig.getDrmType(); @@ -499,13 +499,11 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmCallback, null, mainHandler, this); setViewType(ViewType.TYPE_SURFACEVIEW); } catch (UnsupportedDrmException e) { - Event event = Event.buildErrorEvent(this, e); - fatalError = event.exception; - postEventInternal(event); + fatalError = new SRGDrmMediaPlayerException(e); } } - DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this.context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); + DefaultRenderersFactory renderersFactory = new DefaultRenderersFactory(this.context, drmSessionManager, DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER); exoPlayer = ExoPlayerFactory.newSimpleInstance(renderersFactory, trackSelector, new DefaultLoadControl()); exoPlayer.addListener(this); exoPlayer.setVideoListener(this); @@ -517,6 +515,9 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig audioFocusChangeListener = new OnAudioFocusChangeListener(new WeakReference<>(this)); audioFocusGranted = false; + if (fatalError != null) { + Event event = new Event(this, Event.Type.UNSUPPORTED_DRM, (SRGMediaPlayerException) fatalError); + } } private synchronized void startBackgroundThreadIfNecessary() { From 95fe49a1c5fa60ab10af9d690b7fddf7b9ec747b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:34:32 +0200 Subject: [PATCH 09/23] Fix depreciated warning --- .../src/main/java/ch/srg/mediaplayer/EventLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java index 3a8638e..0254184 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java @@ -59,7 +59,7 @@ /** * Logs player events using {@link Log}. */ -/* package */ final class EventLogger implements ExoPlayer.EventListener, +/* package */ final class EventLogger implements Player.EventListener, AudioRendererEventListener, VideoRendererEventListener, AdaptiveMediaSourceEventListener, ExtractorMediaSource.EventListener, DefaultDrmSessionManager.EventListener, MetadataRenderer.Output { From 67f2fb0ff12e5ecc14dd4284736d45951911c82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 23 Jul 2018 17:34:49 +0200 Subject: [PATCH 10/23] Fix import --- .../src/main/java/ch/srg/mediaplayer/EventLogger.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java index 0254184..bab0ffd 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/EventLogger.java @@ -20,12 +20,12 @@ import android.util.Log; import android.view.Surface; -import com.akamai.android.exoplayer2loader.AkamaiExoPlayerLoader; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; import com.google.android.exoplayer2.Format; import com.google.android.exoplayer2.PlaybackParameters; +import com.google.android.exoplayer2.Player; import com.google.android.exoplayer2.RendererCapabilities; import com.google.android.exoplayer2.Timeline; import com.google.android.exoplayer2.audio.AudioRendererEventListener; From d6e89dfe6472c4f2526731f5c445b0968f8c1915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Tue, 24 Jul 2018 09:11:58 +0200 Subject: [PATCH 11/23] Add Drm error reporting to akamai qos --- .../main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 430f5dd..5cfc677 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -2060,6 +2060,9 @@ public void onDrmKeysLoaded() { public void onDrmSessionManagerError(Exception e) { eventLogger.onDrmSessionManagerError(e); postFatalErrorInternal(new SRGDrmMediaPlayerException(e)); + if (akamaiExoPlayerLoader != null) { + akamaiExoPlayerLoader.onDrmSessionManagerError(e); + } } @Override From 9b459eb2ef8bca3ea1e9d1d55d4ede9bd9ff9a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Tue, 24 Jul 2018 10:38:36 +0200 Subject: [PATCH 12/23] Add DRM Error to the event and Make DRM exception more priority other playback exceptions --- .../ch/srg/mediaplayer/SRGMediaPlayerController.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 5cfc677..f787060 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -225,6 +225,7 @@ public enum Type { STATE_CHANGE, FATAL_ERROR, TRANSIENT_ERROR, /* To be removed ? */ + DRM_ERROR, MEDIA_READY_TO_PLAY, MEDIA_COMPLETED, @@ -499,7 +500,7 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmCallback, null, mainHandler, this); setViewType(ViewType.TYPE_SURFACEVIEW); } catch (UnsupportedDrmException e) { - fatalError = new SRGDrmMediaPlayerException(e); + fatalError = e; } } @@ -1494,10 +1495,19 @@ private void broadcastEvent(Event event) { } private void postFatalErrorInternal(SRGMediaPlayerException e) { + // Don't update fatal error if a Drm exception occurs + if (fatalError instanceof SRGDrmMediaPlayerException || fatalError instanceof UnsupportedDrmException) { + return; + } this.fatalError = e; postEventInternal(Event.buildErrorEvent(this, true, e)); } + private void postDrmErrorInternal(SRGDrmMediaPlayerException e) { + this.fatalError = e; + postEventInternal(new Event(this, Event.Type.DRM_ERROR, e)); + } + private void postEventInternal(Event.Type eventType) { postEventInternal(Event.buildEvent(this, eventType)); } From 9a6430f129efaff126562da2770fe740a97019cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Tue, 24 Jul 2018 11:40:04 +0200 Subject: [PATCH 13/23] Add constructor with no DRMConfig --- .../srg/mediaplayer/SRGMediaPlayerController.java | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index f787060..5640259 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -466,12 +466,24 @@ public interface Listener { private DrmConfig drmConfig; /** - * Create a new SRGMediaPlayerController with the current context, a mediaPlayerDataProvider, and a TAG + * Create a new SRGMediaPlayerController with no DRM support with the current context, a mediaPlayerDataProvider, and a TAG * if you need to retrieve a controller * * @param context context * @param tag tag to identify this controller */ + public SRGMediaPlayerController(Context context, String tag) { + this(context, tag, null); + } + + /** + * Create a new SRGMediaPlayerController with the current context, a mediaPlayerDataProvider, and a TAG + * if you need to retrieve a controller + * + * @param context context + * @param tag tag to identify this controller + * @param drmConfig drm configuration + */ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmConfig) { this.context = context; this.mainHandler = new Handler(Looper.getMainLooper(), this); From 9b7d66bee5a92214b95e0c8941f8e6e63072f560 Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Wed, 25 Jul 2018 12:48:23 +0200 Subject: [PATCH 14/23] Simplify fatal error: drm handled like generic fatal errors except that they cannot be superseded by a player error --- .../mediaplayer/SRGMediaPlayerController.java | 60 +++++++++---------- 1 file changed, 29 insertions(+), 31 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 5640259..8002071 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -163,7 +163,7 @@ public static String getVersion() { private static final int MSG_SET_MUTE = 7; private static final int MSG_APPLY_STATE = 8; private static final int MSG_RELEASE = 9; - private static final int MSG_EXCEPTION = 12; + private static final int MSG_PLAYER_EXCEPTION = 12; private static final int MSG_REGISTER_EVENT_LISTENER = 13; private static final int MSG_UNREGISTER_EVENT_LISTENER = 14; private static final int MSG_PLAYER_PREPARING = 101; @@ -225,7 +225,6 @@ public enum Type { STATE_CHANGE, FATAL_ERROR, TRANSIENT_ERROR, /* To be removed ? */ - DRM_ERROR, MEDIA_READY_TO_PLAY, MEDIA_COMPLETED, @@ -276,9 +275,7 @@ public enum Type { /** * The Segment list has changed. */ - SEGMENT_LIST_CHANGE, - - UNSUPPORTED_DRM + SEGMENT_LIST_CHANGE } public final Type type; @@ -426,7 +423,8 @@ public interface Listener { private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter(); private AudioCapabilities audioCapabilities; private EventLogger eventLogger; - private ViewType viewType; + @NonNull + private ViewType viewType = ViewType.TYPE_TEXTUREVIEW; private View renderingView; private Integer playbackState; private List<Segment> segments = new ArrayList<>(); @@ -482,7 +480,7 @@ public SRGMediaPlayerController(Context context, String tag) { * * @param context context * @param tag tag to identify this controller - * @param drmConfig drm configuration + * @param drmConfig drm configuration null for no DRM support */ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmConfig) { this.context = context; @@ -510,7 +508,7 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig drmSessionManager = new DefaultDrmSessionManager<>(drmType, FrameworkMediaDrm.newInstance(drmType), drmCallback, null, mainHandler, this); - setViewType(ViewType.TYPE_SURFACEVIEW); + viewType = ViewType.TYPE_SURFACEVIEW; } catch (UnsupportedDrmException e) { fatalError = e; } @@ -528,9 +526,6 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig audioFocusChangeListener = new OnAudioFocusChangeListener(new WeakReference<>(this)); audioFocusGranted = false; - if (fatalError != null) { - Event event = new Event(this, Event.Type.UNSUPPORTED_DRM, (SRGMediaPlayerException) fatalError); - } } private synchronized void startBackgroundThreadIfNecessary() { @@ -771,7 +766,7 @@ public boolean handleMessage(final Message msg) { prepareInternal(uri, data.streamType); } catch (SRGMediaPlayerException e) { logE("onUriLoaded", e); - handleFatalExceptionInternal(e); + handlePlayerExceptionInternal(e); } return true; @@ -815,8 +810,8 @@ public boolean handleMessage(final Message msg) { releaseInternal(); return true; - case MSG_EXCEPTION: - handleFatalExceptionInternal((SRGMediaPlayerException) msg.obj); + case MSG_PLAYER_EXCEPTION: + handlePlayerExceptionInternal((SRGMediaPlayerException) msg.obj); return true; case MSG_REGISTER_EVENT_LISTENER: @@ -1178,9 +1173,9 @@ public State getState() { return state; } - private void handleFatalExceptionInternal(SRGMediaPlayerException e) { + private void handlePlayerExceptionInternal(SRGMediaPlayerException e) { logE("exception occurred", e); - postFatalErrorInternal(e); + postFatalErrorInternal(e, false); releaseInternal(); } @@ -1357,8 +1352,10 @@ private void createRenderingViewInMainThread(final Context parentContext) { public void run() { if (viewType == ViewType.TYPE_SURFACEVIEW) { renderingView = new SurfaceView(parentContext); - } else { + } else if (viewType == ViewType.TYPE_TEXTUREVIEW) { renderingView = new TextureView(parentContext); + } else { + throw new IllegalStateException("Unsupported view type: " + viewType); } if (mediaPlayerView != null) { Log.v(TAG, "binding, setVideoRenderingView " + mediaPlayerView); @@ -1432,7 +1429,15 @@ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) { }); } - public void setViewType(ViewType viewType) { + /** + * Warning texture view not supported to play DRM content. + * + * @param viewType view type + */ + public void setViewType(@NonNull ViewType viewType) { + if (debugMode && drmConfig != null && viewType == ViewType.TYPE_TEXTUREVIEW) { + Log.w(TAG, "Texture view does not support DRM"); + } this.viewType = viewType; } @@ -1506,18 +1511,11 @@ private void broadcastEvent(Event event) { sendMessage(MSG_FIRE_EVENT, event); } - private void postFatalErrorInternal(SRGMediaPlayerException e) { - // Don't update fatal error if a Drm exception occurs - if (fatalError instanceof SRGDrmMediaPlayerException || fatalError instanceof UnsupportedDrmException) { - return; + private void postFatalErrorInternal(SRGMediaPlayerException e, boolean override) { + if (override || fatalError != null) { + this.fatalError = e; + postEventInternal(Event.buildErrorEvent(this, true, e)); } - this.fatalError = e; - postEventInternal(Event.buildErrorEvent(this, true, e)); - } - - private void postDrmErrorInternal(SRGDrmMediaPlayerException e) { - this.fatalError = e; - postEventInternal(new Event(this, Event.Type.DRM_ERROR, e)); } private void postEventInternal(Event.Type eventType) { @@ -2036,7 +2034,7 @@ public void onRepeatModeChanged(int repeatMode) { @Override public void onPlayerError(ExoPlaybackException error) { manageKeepScreenOnInternal(); - sendMessage(MSG_EXCEPTION, new SRGMediaPlayerException(error)); + sendMessage(MSG_PLAYER_EXCEPTION, new SRGMediaPlayerException(error)); } @Override @@ -2081,7 +2079,7 @@ public void onDrmKeysLoaded() { @Override public void onDrmSessionManagerError(Exception e) { eventLogger.onDrmSessionManagerError(e); - postFatalErrorInternal(new SRGDrmMediaPlayerException(e)); + postFatalErrorInternal(new SRGDrmMediaPlayerException(e), true); if (akamaiExoPlayerLoader != null) { akamaiExoPlayerLoader.onDrmSessionManagerError(e); } From e34cff014b09724d2fe42e8670864d73ecde96b5 Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Wed, 25 Jul 2018 12:48:32 +0200 Subject: [PATCH 15/23] DRM requires API level 18 --- .../java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 8002071..f42a8e1 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -60,6 +60,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -501,7 +502,7 @@ public SRGMediaPlayerController(Context context, String tag, @Nullable DrmConfig eventLogger = new EventLogger(trackSelector); DrmSessionManager<FrameworkMediaCrypto> drmSessionManager = null; UnsupportedDrmException unsupportedDrm = null; - if (drmConfig != null) { + if (drmConfig != null && Util.SDK_INT >= 18) { try { UUID drmType = drmConfig.getDrmType(); HttpMediaDrmCallback drmCallback = new HttpMediaDrmCallback(drmConfig.getLicenceUrl(), new DefaultHttpDataSourceFactory(userAgent)); @@ -2105,4 +2106,8 @@ public void onDrmKeysRemoved() { public void setAkamaiMediaAnalyticsConfiguration(@Nullable AkamaiMediaAnalyticsConfiguration akamaiMediaAnalyticsConfiguration) { this.akamaiMediaAnalyticsConfiguration = akamaiMediaAnalyticsConfiguration; } + + public static boolean isDrmSupported() { + return Util.SDK_INT >= 18; + } } \ No newline at end of file From 24e24642df010d958b109ccc14bbf7b993a5031d Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Wed, 25 Jul 2018 13:02:38 +0200 Subject: [PATCH 16/23] Fix fatal errors --- .../main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index f42a8e1..230ddd1 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -1513,7 +1513,7 @@ private void broadcastEvent(Event event) { } private void postFatalErrorInternal(SRGMediaPlayerException e, boolean override) { - if (override || fatalError != null) { + if (override || fatalError == null) { this.fatalError = e; postEventInternal(Event.buildErrorEvent(this, true, e)); } From e473b038aa5077e4129126c1724b8a1ef2226edd Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Wed, 25 Jul 2018 13:03:22 +0200 Subject: [PATCH 17/23] Expose HTTP 403 errors as forbidden errors to show different error message --- .../mediaplayer/SRGMediaPlayerController.java | 10 +++++++++- .../SRGMediaPlayerForbiddenException.java | 19 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerForbiddenException.java diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 230ddd1..5a50f89 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -60,6 +60,7 @@ import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; import com.google.android.exoplayer2.upstream.FileDataSourceFactory; +import com.google.android.exoplayer2.upstream.HttpDataSource; import com.google.android.exoplayer2.util.Util; import java.lang.annotation.Retention; @@ -2035,7 +2036,14 @@ public void onRepeatModeChanged(int repeatMode) { @Override public void onPlayerError(ExoPlaybackException error) { manageKeepScreenOnInternal(); - sendMessage(MSG_PLAYER_EXCEPTION, new SRGMediaPlayerException(error)); + Throwable cause = error.getCause(); + SRGMediaPlayerException exception = new SRGMediaPlayerException(error); + if (cause instanceof HttpDataSource.InvalidResponseCodeException) { + if (((HttpDataSource.InvalidResponseCodeException) cause).responseCode == 403) { + exception = new SRGMediaPlayerForbiddenException(error); + } + } + sendMessage(MSG_PLAYER_EXCEPTION, exception); } @Override diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerForbiddenException.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerForbiddenException.java new file mode 100644 index 0000000..80fae5e --- /dev/null +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerForbiddenException.java @@ -0,0 +1,19 @@ +package ch.srg.mediaplayer; + +/** + * Created by Axel on 02/03/2015. + */ +public class SRGMediaPlayerForbiddenException extends SRGMediaPlayerException { + + public SRGMediaPlayerForbiddenException(String detailMessage) { + super(detailMessage); + } + + public SRGMediaPlayerForbiddenException(String detailMessage, Throwable throwable) { + super(detailMessage, throwable); + } + + public SRGMediaPlayerForbiddenException(Throwable throwable) { + super(throwable); + } +} From 42aad506688546b3883ebf5d25480833b912a616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Wed, 25 Jul 2018 16:45:11 +0200 Subject: [PATCH 18/23] Remove work arround for index out of bound exception --- .../main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 5a50f89..ccf47d2 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -46,6 +46,7 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; +import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; From 9e1842105c88848e021f5477e78f1aafcb8e033d Mon Sep 17 00:00:00 2001 From: sebastien <sebastien.chauvin@gmail.com> Date: Wed, 15 Aug 2018 11:46:43 +0200 Subject: [PATCH 19/23] Support playback starting with a user selected segment --- .../mediaplayer/SRGMediaPlayerController.java | 46 ++++++++++++++++--- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index ccf47d2..8b70e90 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -613,22 +613,50 @@ public boolean play(@NonNull Uri uri, Long startPositionMs, @SRGStreamType int s * The corresponding events are triggered when the video loading start and is ready. * * @param uri uri of the media - * @param startPositionMs start position in milliseconds or null to prevent seek + * @param startPositionMs start position in milliseconds relative to uri or segment when given or null to prevent seek * @param streamType {@link SRGMediaPlayerController#STREAM_DASH}, {@link SRGMediaPlayerController#STREAM_HLS}, {@link SRGMediaPlayerController#STREAM_HTTP_PROGRESSIVE} or {@link SRGMediaPlayerController#STREAM_LOCAL_FILE} - * @throws SRGMediaPlayerException player exception + * @param segments logical segment list + * @param segment segment to play, must be in segments list. This is considered a user selected segment (SEGMENT_SELECTED is sent) + * @throws IllegalArgumentException if segment is not in segment list or uri is null */ @SuppressWarnings("ConstantConditions") public void prepare(@NonNull Uri uri, Long startPositionMs, @SRGStreamType int streamType, - List<Segment> segments) throws SRGMediaPlayerException { + List<Segment> segments, + Segment segment) { if (uri == null) { throw new IllegalArgumentException("Invalid argument: null uri"); } - PrepareUriData data = new PrepareUriData(uri, startPositionMs, streamType, segments); + if (segment != null && !segments.contains(segment)) { + throw new IllegalArgumentException("Unknown segment: " + segment); + } + PrepareUriData data = new PrepareUriData(uri, startPositionMs, streamType, segments, segment); sendMessage(MSG_PREPARE_FOR_URI, data); } + /** + * Try to play a video with a url and corresponding segments, you can't replay the current playing video. + * will throw an exception if you haven't setup a data provider or if the media is not present + * in the provider. + * <p/> + * The corresponding events are triggered when the video loading start and is ready. + * + * @param uri uri of the media + * @param startPositionMs start position in milliseconds or null to prevent seek + * @param streamType {@link SRGMediaPlayerController#STREAM_DASH}, {@link SRGMediaPlayerController#STREAM_HLS}, {@link SRGMediaPlayerController#STREAM_HTTP_PROGRESSIVE} or {@link SRGMediaPlayerController#STREAM_LOCAL_FILE} + * @param segments logical segment list + * @throws IllegalArgumentException if segment is not in segment list or uri is null + * @ player exception + */ + @SuppressWarnings("ConstantConditions") + public void prepare(@NonNull Uri uri, + Long startPositionMs, + @SRGStreamType int streamType, + List<Segment> segments) { + prepare(uri, startPositionMs, streamType, segments, null); + } + public void keepScreenOn(boolean lock) { externalWakeLock = lock; manageKeepScreenOnInternal(); @@ -639,12 +667,14 @@ class PrepareUriData { Long position; int streamType; private List<Segment> segments; + private Segment segment; - PrepareUriData(Uri uri, Long position, int streamType, List<Segment> segments) { + PrepareUriData(Uri uri, Long position, int streamType, List<Segment> segments, Segment segment) { this.uri = uri; this.position = position; this.streamType = streamType; this.segments = segments; + this.segment = segment; } @Override @@ -754,11 +784,15 @@ public boolean handleMessage(final Message msg) { setStateInternal(State.PREPARING); PrepareUriData data = (PrepareUriData) msg.obj; Uri uri = data.uri; - this.segments.clear(); seekToWhenReady = data.position; + this.segments.clear(); currentSegment = null; if (data.segments != null) { segments.addAll(data.segments); + if (data.segment != null) { + postEventInternal(new Event(this, Event.Type.SEGMENT_SELECTED, null, data.segment)); + seekToWhenReady = data.position + data.segment.getMarkIn(); + } } postEventInternal(Event.Type.SEGMENT_LIST_CHANGE); postEventInternal(Event.Type.MEDIA_READY_TO_PLAY); From 3c84e605c006fe7c8a9a623343654100a77c78cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Thu, 16 Aug 2018 11:47:12 +0200 Subject: [PATCH 20/23] Use update dash manifest workaround to avoid application crash when there is a problem with the manifest --- .../java/ch/srg/mediaplayer/DefaultDashChunkSource.java | 2 ++ .../java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java index c7cd217..5bb7ff0 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java @@ -56,6 +56,7 @@ import java.util.List; /** + * Work around to avoid application crash due to an OutOfBoundException * A default {@link DashChunkSource} implementation. */ public class DefaultDashChunkSource implements DashChunkSource { @@ -154,6 +155,7 @@ public void updateManifest(DashManifest newManifest, int newPeriodIndex) { List<Representation> representations = getRepresentations(); for (int i = 0; i < representationHolders.length; i++) { int index = trackSelection.getIndexInTrackGroup(i); + // FIXME : workaround, check if still needed with 2.8.2 if (index >= representations.size()) { index = 0; Log.e("DefaultDashChunkSource", "invalid track index " + index + " (" + representations.size() + ") "); diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 8b70e90..bc29429 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -46,7 +46,6 @@ import com.google.android.exoplayer2.source.TrackGroup; import com.google.android.exoplayer2.source.TrackGroupArray; import com.google.android.exoplayer2.source.dash.DashMediaSource; -import com.google.android.exoplayer2.source.dash.DefaultDashChunkSource; import com.google.android.exoplayer2.source.hls.HlsMediaSource; import com.google.android.exoplayer2.text.Cue; import com.google.android.exoplayer2.text.TextRenderer; @@ -791,7 +790,7 @@ public boolean handleMessage(final Message msg) { segments.addAll(data.segments); if (data.segment != null) { postEventInternal(new Event(this, Event.Type.SEGMENT_SELECTED, null, data.segment)); - seekToWhenReady = data.position + data.segment.getMarkIn(); + seekToWhenReady = (data.position != null ? data.position : 0) + data.segment.getMarkIn(); } } postEventInternal(Event.Type.SEGMENT_LIST_CHANGE); @@ -959,8 +958,9 @@ private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMe switch (streamType) { case STREAM_DASH: + // Use DefaultDashChunkSource with workaround that don't crash the application if problem during manifest parsing mediaSource = new DashMediaSource(videoUri, new DefaultHttpDataSourceFactory(userAgent), - new DefaultDashChunkSource.Factory(dataSourceFactory), mainHandler, eventLogger); + new ch.srg.mediaplayer.DefaultDashChunkSource.Factory(dataSourceFactory), mainHandler, eventLogger); break; case STREAM_HLS: mediaSource = new HlsMediaSource(videoUri, dataSourceFactory, mainHandler, eventLogger); From e8325b2b7e3632988c4441bba1988cc149e12a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Thu, 16 Aug 2018 12:04:01 +0200 Subject: [PATCH 21/23] Fix log message showing the bad index number --- .../main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java index 5bb7ff0..a0f18f2 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/DefaultDashChunkSource.java @@ -157,8 +157,8 @@ public void updateManifest(DashManifest newManifest, int newPeriodIndex) { int index = trackSelection.getIndexInTrackGroup(i); // FIXME : workaround, check if still needed with 2.8.2 if (index >= representations.size()) { - index = 0; Log.e("DefaultDashChunkSource", "invalid track index " + index + " (" + representations.size() + ") "); + index = 0; } Representation representation = representations.get(index); representationHolders[i].updateRepresentation(periodDurationUs, representation); From 6354c3b9933d0a394a6aa5e636927efee4d4a9ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Fri, 17 Aug 2018 09:12:24 +0200 Subject: [PATCH 22/23] Documentation exoplayer github issue --- .../main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java | 1 + 1 file changed, 1 insertion(+) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index bc29429..317b93b 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -959,6 +959,7 @@ private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMe switch (streamType) { case STREAM_DASH: // Use DefaultDashChunkSource with workaround that don't crash the application if problem during manifest parsing + // https://github.com/google/ExoPlayer/issues/2795 mediaSource = new DashMediaSource(videoUri, new DefaultHttpDataSourceFactory(userAgent), new ch.srg.mediaplayer.DefaultDashChunkSource.Factory(dataSourceFactory), mainHandler, eventLogger); break; From ca788e08a3abd4d49efaa16a2f6a494ca205c49d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= <joaquim.stahli@rts.ch> Date: Mon, 20 Aug 2018 14:59:28 +0200 Subject: [PATCH 23/23] Fix #160 glitch when play media at position --- .../mediaplayer/SRGMediaPlayerController.java | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java index 317b93b..c386213 100644 --- a/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java +++ b/srgmediaplayer/src/main/java/ch/srg/mediaplayer/SRGMediaPlayerController.java @@ -783,14 +783,14 @@ public boolean handleMessage(final Message msg) { setStateInternal(State.PREPARING); PrepareUriData data = (PrepareUriData) msg.obj; Uri uri = data.uri; - seekToWhenReady = data.position; + Long playbackStartPosition = data.position; this.segments.clear(); currentSegment = null; if (data.segments != null) { segments.addAll(data.segments); if (data.segment != null) { postEventInternal(new Event(this, Event.Type.SEGMENT_SELECTED, null, data.segment)); - seekToWhenReady = (data.position != null ? data.position : 0) + data.segment.getMarkIn(); + playbackStartPosition = (data.position != null ? data.position : 0) + data.segment.getMarkIn(); } } postEventInternal(Event.Type.SEGMENT_LIST_CHANGE); @@ -799,7 +799,7 @@ public boolean handleMessage(final Message msg) { if (mediaPlayerView != null) { internalUpdateMediaPlayerViewBound(); } - prepareInternal(uri, data.streamType); + prepareInternal(uri, playbackStartPosition, data.streamType); } catch (SRGMediaPlayerException e) { logE("onUriLoaded", e); handlePlayerExceptionInternal(e); @@ -932,7 +932,7 @@ public void run() { } } - private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMediaPlayerException { + private void prepareInternal(@NonNull Uri videoUri, @Nullable Long playbackStartPosition, int streamType) throws SRGMediaPlayerException { Log.v(TAG, "Preparing " + videoUri + " (" + streamType + ")"); setupAkamaiQos(videoUri); try { @@ -980,6 +980,15 @@ private void prepareInternal(@NonNull Uri videoUri, int streamType) throws SRGMe } exoPlayer.prepare(mediaSource); + if (playbackStartPosition != null) { + try { + exoPlayer.seekTo(playbackStartPosition); + checkSegmentChange(playbackStartPosition); // Done here ? + } catch (IllegalStateException ignored) { + Log.w(TAG, "Invalid initial playback position", ignored); + } + } + } catch (Exception e) { release(); throw new SRGMediaPlayerException(e);