From f0569cc7f55c15caec37073ef2443695e864aa07 Mon Sep 17 00:00:00 2001 From: Koen J Date: Fri, 5 Dec 2025 10:24:23 +0100 Subject: [PATCH] Further work. --- .../futo/platformplayer/sabr/ITagUtils.java | 109 ++++++++++ .../futo/platformplayer/sabr/MediaFormat.java | 45 +++++ .../sabr/MediaFormatComparator.java | 60 ++++++ .../platformplayer/sabr/MediaFormatUtils.java | 122 +++++++++++ .../sabr/MediaItemFormatInfo.java | 58 ++++++ .../sabr/MediaItemStoryboard.java | 15 ++ .../platformplayer/sabr/MediaSubtitle.java | 20 ++ .../platformplayer/sabr/SabrMediaPeriod.java | 84 ++++++-- .../platformplayer/sabr/SabrMediaSource.java | 104 +++++++--- .../sabr/manifest/SabrManifestParser.java | 191 +++++++++++------- 10 files changed, 692 insertions(+), 116 deletions(-) create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/ITagUtils.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaFormat.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaFormatComparator.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaFormatUtils.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaItemFormatInfo.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaItemStoryboard.java create mode 100644 app/src/main/java/com/futo/platformplayer/sabr/MediaSubtitle.java diff --git a/app/src/main/java/com/futo/platformplayer/sabr/ITagUtils.java b/app/src/main/java/com/futo/platformplayer/sabr/ITagUtils.java new file mode 100644 index 00000000..f5af3e2c --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/ITagUtils.java @@ -0,0 +1,109 @@ +package com.futo.platformplayer.sabr; + +import java.util.Arrays; +import java.util.List; + +public final class ITagUtils { + public final static String AUDIO_68K_WEBM = "249"; + public final static String AUDIO_89K_WEBM = "250"; + public final static String AUDIO_133K_WEBM = "171"; + public final static String AUDIO_156K_WEBM = "251"; + public final static String AUDIO_48K_AAC = "139"; + public final static String AUDIO_128K_AAC = "140"; + public final static String VIDEO_144P_WEBM = "278"; + public final static String VIDEO_144P_AVC = "160"; + public final static String VIDEO_240P_WEBM = "242"; + public final static String VIDEO_240P_AVC = "133"; + public final static String VIDEO_360P_WEBM = "243"; + public final static String VIDEO_360P_AVC = "134"; + public final static String VIDEO_480P_WEBM = "244"; + public final static String VIDEO_480P_AVC = "135"; + public final static String VIDEO_720P_WEBM = "247"; + public final static String VIDEO_720P_WEBM_60FPS_HDR = "334"; + public final static String VIDEO_720P_AVC = "136"; + public final static String VIDEO_720P_AVC_60FPS = "298"; + public final static String VIDEO_1080P_WEBM = "248"; + public final static String VIDEO_1080P_WEBM_60FPS_HDR = "335"; + public final static String VIDEO_1080P_AVC = "137"; + public final static String VIDEO_1080P_AVC_60FPS = "299"; + public final static String VIDEO_1440P_WEBM = "271"; + public final static String VIDEO_1440P_WEBM_60FPS_HDR = "336"; + public final static String VIDEO_1440P_WEBM_60FPS = "308"; + public final static String VIDEO_1440P_AVC = "264"; + public final static String VIDEO_2160P_WEBM = "313"; + public final static String VIDEO_2160P_WEBM_60FPS_HDR = "337"; + public final static String VIDEO_2160P_WEBM_60FPS = "315"; + public final static String VIDEO_2160P_AVC = "266"; + public final static String VIDEO_2160P_AVC_HQ = "138"; + + public final static String MUXED_360P_WEBM = "43"; + public final static String MUXED_360P_AVC = "18"; + public final static String MUXED_720P_AVC = "22"; + + private final static List sOrderedITagsAVC = Arrays.asList( + MUXED_360P_AVC, MUXED_720P_AVC, + AUDIO_48K_AAC, AUDIO_128K_AAC, + VIDEO_144P_AVC, VIDEO_240P_AVC, + VIDEO_360P_AVC, VIDEO_480P_AVC, VIDEO_720P_AVC, VIDEO_720P_AVC_60FPS, + VIDEO_1080P_AVC, VIDEO_1080P_AVC_60FPS, VIDEO_1440P_AVC, VIDEO_2160P_AVC, VIDEO_2160P_AVC_HQ); + + private final static List sOrderedITagsWEBM = Arrays.asList( + MUXED_360P_WEBM, + AUDIO_68K_WEBM, AUDIO_89K_WEBM, AUDIO_133K_WEBM, AUDIO_156K_WEBM, + VIDEO_144P_WEBM, VIDEO_240P_WEBM, + VIDEO_360P_WEBM, VIDEO_480P_WEBM, VIDEO_720P_WEBM, VIDEO_720P_WEBM_60FPS_HDR, + VIDEO_1080P_WEBM, VIDEO_1080P_WEBM_60FPS_HDR, VIDEO_1440P_WEBM, VIDEO_1440P_WEBM_60FPS_HDR, VIDEO_1440P_WEBM_60FPS, + VIDEO_2160P_WEBM, VIDEO_2160P_WEBM_60FPS_HDR, VIDEO_2160P_WEBM_60FPS); + + private final static List> sITagsContainer = Arrays.asList(sOrderedITagsAVC, sOrderedITagsWEBM); + public static final String AVC = "AVC"; + public static final String WEBM = "VP9"; + + public static int compare(String leftITag, String rightITag) { + for (List iTags : sITagsContainer) { + int left = iTags.indexOf(leftITag); + int right = iTags.indexOf(rightITag); + if (left != -1 && right != -1) { + return left - right; + } + } + + // TODO: we can't be here + return 99; + } + + public static boolean belongsToType(String type, String iTag) { + String realType = getRealType(iTag); + return type.equals(realType); + } + + public static boolean belongsToType(String type, int iTag) { + String realType = getRealType(String.valueOf(iTag)); + return type.equals(realType); + } + + private static String getRealType(String iTag) { + if (sOrderedITagsAVC.contains(iTag)) { + return AVC; + } + return WEBM; + } + + public static String getAudioRateByTag(String iTag) { + switch (iTag) { + case AUDIO_128K_AAC: + return "44100"; + case AUDIO_48K_AAC: + return "22050"; + case AUDIO_156K_WEBM: + return "48000"; + case AUDIO_133K_WEBM: + return "44100"; + case AUDIO_89K_WEBM: + return "48000"; + case AUDIO_68K_WEBM: + return "48000"; + } + return "44100"; + } +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaFormat.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormat.java new file mode 100644 index 00000000..9e760188 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormat.java @@ -0,0 +1,45 @@ +package com.futo.platformplayer.sabr; + +import java.util.List; + +public interface MediaFormat extends Comparable { + int FORMAT_TYPE_DASH = 0; + int FORMAT_TYPE_REGULAR = 1; + int FORMAT_TYPE_SABR = 2; + // Common + int getFormatType(); + String getUrl(); + String getMimeType(); + String getITag(); + boolean isDrc(); + + // DASH + String getClen(); + String getBitrate(); + String getProjectionType(); + String getXtags(); + int getWidth(); + int getHeight(); + String getIndex(); + String getInit(); + String getFps(); + String getLmt(); + String getQualityLabel(); + String getFormat(); + boolean isOtf(); + String getOtfInitUrl(); + String getOtfTemplateUrl(); + String getLanguage(); + // DASH LIVE + String getTargetDurationSec(); + String getLastModified(); + String getMaxDvrDurationSec(); + + // Other/Regular + String getQuality(); + String getSignature(); + String getAudioSamplingRate(); + String getSourceUrl(); + List getSegmentUrlList(); + List getGlobalSegmentList(); +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatComparator.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatComparator.java new file mode 100644 index 00000000..60d285b5 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatComparator.java @@ -0,0 +1,60 @@ +package com.futo.platformplayer.sabr; + +import java.util.Comparator; + +public class MediaFormatComparator implements Comparator { + public static final int ORDER_DESCENDANT = 0; + public static final int ORDER_ASCENDANT = 1; + private int mOrderType = ORDER_DESCENDANT; + + public MediaFormatComparator() { + + } + + public MediaFormatComparator(int orderType) { + mOrderType = orderType; + } + + /** + * NOTE: Descendant sorting (better on top). High quality playback on external player. + */ + @Override + public int compare(MediaFormat leftItem, MediaFormat rightItem) { + if (leftItem.getGlobalSegmentList() != null || + rightItem.getGlobalSegmentList() != null) { + return 1; + } + + if (mOrderType == ORDER_ASCENDANT) { + MediaFormat tmpItem = leftItem; + leftItem = rightItem; + rightItem = tmpItem; + } + + int leftItemBitrate = leftItem.getBitrate() == null ? 0 : parseInt(leftItem.getBitrate()); + int rightItemBitrate = rightItem.getBitrate() == null ? 0 : parseInt(rightItem.getBitrate()); + + int leftItemHeight = leftItem.getHeight(); + int rightItemHeight = rightItem.getHeight(); + + int delta = rightItemHeight - leftItemHeight; + + if (delta == 0) { + delta = rightItemBitrate - leftItemBitrate; + } + + return delta; + } + + public static boolean isNumeric(String s) { + return s != null && s.matches("^[-+]?\\d*\\.?\\d+$"); + } + + private int parseInt(String num) { + if (!isNumeric(num)) { + return 0; + } + + return Integer.parseInt(num); + } +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatUtils.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatUtils.java new file mode 100644 index 00000000..09fd81a7 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaFormatUtils.java @@ -0,0 +1,122 @@ +package com.futo.platformplayer.sabr; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class MediaFormatUtils { + public static final String MIME_WEBM_AUDIO = "audio/webm"; + public static final String MIME_WEBM_VIDEO = "video/webm"; + public static final String MIME_MP4_AUDIO = "audio/mp4"; + public static final String MIME_MP4_VIDEO = "video/mp4"; + private static final Pattern CODECS_PATTERN = Pattern.compile(".*codecs=\\\"(.*)\\\""); + + public static boolean isNumeric(String s) { + return s != null && s.matches("^[-+]?\\d*\\.?\\d+$"); + } + + public static boolean isDash(String id) { + if (!isNumeric(id)) { + return false; + } + + int maxRegularITag = 50; + int itag = Integer.parseInt(id); + + return itag > maxRegularITag; + } + + public static boolean isDash(MediaFormat format) { + if (format.getITag() == null) { + return false; + } + + if (format.getGlobalSegmentList() != null) { + return true; + } + + String id = format.getITag(); + + return isDash(id); + } + + public static boolean checkMediaUrl(MediaFormat format) { + return format != null && format.getUrl() != null; + } + + public static String extractMimeType(MediaFormat format) { + if (format.getGlobalSegmentList() != null) { + return format.getMimeType(); + } + + String codecs = extractCodecs(format); + + if (codecs.startsWith("vorbis") || + codecs.startsWith("opus")) { + return MIME_WEBM_AUDIO; + } + + if (codecs.startsWith("vp9") || + codecs.startsWith("vp09")) { + return MIME_WEBM_VIDEO; + } + + if (codecs.startsWith("mp4a") || + codecs.startsWith("ec-3") || + codecs.startsWith("ac-3")) { + return MIME_MP4_AUDIO; + } + + if (codecs.startsWith("avc") || + codecs.startsWith("av01")) { + return MIME_MP4_VIDEO; + } + + return null; + } + + public static String extractCodecs(MediaFormat format) { + // input example: video/mp4;+codecs="avc1.640033" + Matcher matcher = CODECS_PATTERN.matcher(format.getMimeType()); + matcher.find(); + return matcher.group(1); + } + + public static boolean isLiveMedia(MediaFormat format) { + boolean isLive = + format.getUrl().contains("live=1") || + format.getUrl().contains("yt_live_broadcast"); + + return isLive; + } + + private static String normalize(String word) { + if (word == null || word.isEmpty()) { + return word; + } + + return word.toLowerCase().replace("ё", "е"); + } + + public static boolean startsWith(String word, String prefix) { + if (word == null && prefix == null) { + return true; + } + + if (word == null || prefix == null) { + return false; + } + + word = normalize(word); + prefix = normalize(prefix); + + return word.startsWith(prefix); + } + + public static boolean isAudio(String mimeType) { + return startsWith(mimeType, "audio"); + } + + public static boolean isVideo(String mimeType) { + return startsWith(mimeType, "video"); + } +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaItemFormatInfo.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaItemFormatInfo.java new file mode 100644 index 00000000..987e865e --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaItemFormatInfo.java @@ -0,0 +1,58 @@ +package com.futo.platformplayer.sabr; + +import java.io.InputStream; +import java.util.List; + +public interface MediaItemFormatInfo { + List getAdaptiveFormats(); + List getUrlFormats(); + List getSubtitles(); + String getHlsManifestUrl(); + String getDashManifestUrl(); + // video metadata + String getLengthSeconds(); + String getTitle(); + String getAuthor(); + String getViewCount(); + String getDescription(); + String getVideoId(); + String getChannelId(); + boolean isLive(); + boolean isLiveContent(); + boolean containsMedia(); + boolean containsSabrFormats(); + boolean containsDashFormats(); + boolean containsHlsUrl(); + boolean containsDashUrl(); + boolean containsUrlFormats(); + boolean hasExtendedHlsFormats(); + float getVolumeLevel(); + InputStream createMpdStream(); + //Observable createMpdStreamObservable(); + List createUrlList(); + MediaItemStoryboard createStoryboard(); + boolean isUnplayable(); + boolean isUnknownError(); + String getPlayabilityStatus(); + boolean isStreamSeekable(); + /** + * Stream start time in UTC (!!!).
+ * E.g.: 2021-10-06T13:36:25+00:00 + */ + String getStartTimestamp(); + String getUploadDate(); + /** + * Stream start time in UNIX format.
+ */ + long getStartTimeMs(); + /** + * Number of the stream first segment + */ + int getStartSegmentNum(); + /** + * Precise segment duration.
+ * Used inside live streams + */ + int getSegmentDurationUs(); + String getPaidContentText(); +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaItemStoryboard.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaItemStoryboard.java new file mode 100644 index 00000000..968b18c9 --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaItemStoryboard.java @@ -0,0 +1,15 @@ +package com.futo.platformplayer.sabr; + +public interface MediaItemStoryboard { + int getGroupDurationMS(); + Size getGroupSize(); + String getGroupUrl(int imgNum); + interface Size { + int getDurationEachMS(); + int getStartNum(); + int getWidth(); + int getHeight(); + int getRowCount(); + int getColCount(); + } +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/MediaSubtitle.java b/app/src/main/java/com/futo/platformplayer/sabr/MediaSubtitle.java new file mode 100644 index 00000000..9f98a6ae --- /dev/null +++ b/app/src/main/java/com/futo/platformplayer/sabr/MediaSubtitle.java @@ -0,0 +1,20 @@ +package com.futo.platformplayer.sabr; + +public interface MediaSubtitle { + String getBaseUrl(); + void setBaseUrl(String baseUrl); + boolean isTranslatable(); + void setTranslatable(boolean translatable); + String getLanguageCode(); + void setLanguageCode(String languageCode); + String getVssId(); + void setVssId(String vssId); + String getName(); + void setName(String name); + String getMimeType(); + void setMimeType(String mimeType); + String getCodecs(); + void setCodecs(String codecs); + String getType(); + void setType(String type); +} diff --git a/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaPeriod.java b/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaPeriod.java index 8caa94dd..163c0bfa 100644 --- a/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaPeriod.java +++ b/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaPeriod.java @@ -9,7 +9,10 @@ import androidx.annotation.Nullable; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.LoadingInfo; import androidx.media3.exoplayer.SeekParameters; +import androidx.media3.exoplayer.drm.DrmSessionEventListener; +import androidx.media3.exoplayer.drm.DrmSessionManager; import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory; import androidx.media3.exoplayer.source.EmptySampleStream; import androidx.media3.exoplayer.source.MediaPeriod; @@ -72,6 +75,8 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< private int periodIndex; private List eventStreams; private boolean notifiedReadingStarted; + private final DrmSessionManager drmSessionManager; + private final DrmSessionEventListener.EventDispatcher drmEventDispatcher; public SabrMediaPeriod( int id, @@ -107,6 +112,13 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< Pair result = buildTrackGroups(period.adaptationSets); trackGroups = result.first; trackGroupInfos = result.second; + this.drmSessionManager = DrmSessionManager.DRM_UNSUPPORTED; + this.drmEventDispatcher = new DrmSessionEventListener.EventDispatcher(); + } + + @Override + public boolean isLoading() { + return compositeSequenceableLoader.isLoading(); } @Override @@ -170,7 +182,6 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< @Override public long readDiscontinuity() { if (!notifiedReadingStarted) { - eventDispatcher.readingStarted(); notifiedReadingStarted = true; } return C.TIME_UNSET; @@ -208,8 +219,8 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< } @Override - public boolean continueLoading(long positionUs) { - return compositeSequenceableLoader.continueLoading(positionUs); + public boolean continueLoading(LoadingInfo loadingInfo) { + return compositeSequenceableLoader.continueLoading(loadingInfo); } @Override @@ -255,9 +266,9 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< sampleStream.release(this); } callback = null; - eventDispatcher.mediaPeriodReleased(); } + @SuppressWarnings("unchecked") private static ChunkSampleStream[] newSampleStreamArray(int length) { return new ChunkSampleStream[length]; @@ -385,8 +396,11 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< eventMessageTrackGroupIndex, cea608TrackGroupIndex); if (eventMessageTrackGroupIndex != C.INDEX_UNSET) { - Format format = Format.createSampleFormat(firstAdaptationSet.id + ":emsg", - MimeTypes.APPLICATION_EMSG, null, Format.NO_VALUE, null); + Format format = + new Format.Builder() + .setId(firstAdaptationSet.id + ":emsg") + .setSampleMimeType(MimeTypes.APPLICATION_EMSG) + .build(); trackGroups[eventMessageTrackGroupIndex] = new TrackGroup(format); trackGroupInfos[eventMessageTrackGroupIndex] = TrackGroupInfo.embeddedEmsgTrack(adaptationSetIndices, primaryTrackGroupIndex); @@ -526,7 +540,11 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< } } - private ChunkSampleStream buildSampleStream(TrackGroupInfo trackGroupInfo, ExoTrackSelection selection, long positionUs) { + private ChunkSampleStream buildSampleStream( + TrackGroupInfo trackGroupInfo, + ExoTrackSelection selection, + long positionUs) { + int embeddedTrackCount = 0; boolean enableEventMessageTrack = trackGroupInfo.embeddedEventMessageTrackGroupIndex != C.INDEX_UNSET; @@ -536,34 +554,72 @@ final class SabrMediaPeriod implements MediaPeriod, SequenceableLoader.Callback< trackGroups.get(trackGroupInfo.embeddedEventMessageTrackGroupIndex); embeddedTrackCount++; } - boolean enableCea608Tracks = trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET; + boolean enableCea608Tracks = + trackGroupInfo.embeddedCea608TrackGroupIndex != C.INDEX_UNSET; TrackGroup embeddedCea608TrackGroup = null; if (enableCea608Tracks) { - embeddedCea608TrackGroup = trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex); + embeddedCea608TrackGroup = + trackGroups.get(trackGroupInfo.embeddedCea608TrackGroupIndex); embeddedTrackCount += embeddedCea608TrackGroup.length; } Format[] embeddedTrackFormats = new Format[embeddedTrackCount]; int[] embeddedTrackTypes = new int[embeddedTrackCount]; embeddedTrackCount = 0; + if (enableEventMessageTrack) { - embeddedTrackFormats[embeddedTrackCount] = embeddedEventMessageTrackGroup.getFormat(0); + embeddedTrackFormats[embeddedTrackCount] = + embeddedEventMessageTrackGroup.getFormat(0); embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_METADATA; embeddedTrackCount++; } + List embeddedCea608TrackFormats = new ArrayList<>(); if (enableCea608Tracks) { for (int i = 0; i < embeddedCea608TrackGroup.length; i++) { - embeddedTrackFormats[embeddedTrackCount] = embeddedCea608TrackGroup.getFormat(i); + embeddedTrackFormats[embeddedTrackCount] = + embeddedCea608TrackGroup.getFormat(i); embeddedTrackTypes[embeddedTrackCount] = C.TRACK_TYPE_TEXT; embeddedCea608TrackFormats.add(embeddedTrackFormats[embeddedTrackCount]); embeddedTrackCount++; } } - PlayerTrackEmsgHandler trackPlayerEmsgHandler = manifest.dynamic && enableEventMessageTrack ? playerEmsgHandler.newPlayerTrackEmsgHandler() : null; - SabrChunkSource chunkSource = chunkSourceFactory.createSabrChunkSource(manifestLoaderErrorThrower, manifest, periodIndex, trackGroupInfo.adaptationSetIndices, selection, trackGroupInfo.trackType, elapsedRealtimeOffsetMs, enableEventMessageTrack, embeddedCea608TrackFormats, trackPlayerEmsgHandler, transferListener); - ChunkSampleStream stream = new ChunkSampleStream<>(trackGroupInfo.trackType, embeddedTrackTypes, embeddedTrackFormats, chunkSource, this, allocator, positionUs, loadErrorHandlingPolicy, eventDispatcher); + PlayerTrackEmsgHandler trackPlayerEmsgHandler = + manifest.dynamic && enableEventMessageTrack + ? playerEmsgHandler.newPlayerTrackEmsgHandler() + : null; + + SabrChunkSource chunkSource = + chunkSourceFactory.createSabrChunkSource( + manifestLoaderErrorThrower, + manifest, + periodIndex, + trackGroupInfo.adaptationSetIndices, + selection, + trackGroupInfo.trackType, + elapsedRealtimeOffsetMs, + enableEventMessageTrack, + embeddedCea608TrackFormats, + trackPlayerEmsgHandler, + transferListener); + + ChunkSampleStream stream = + new ChunkSampleStream<>( + trackGroupInfo.trackType, + embeddedTrackTypes, + embeddedTrackFormats, + chunkSource, + /* callback= */ this, + allocator, + positionUs, + drmSessionManager, + drmEventDispatcher, + loadErrorHandlingPolicy, + eventDispatcher, + /* canReportInitialDiscontinuity= */ true, + /* downloadExecutor= */ null); + synchronized (this) { // The map is also accessed on the loading thread so synchronize access. trackEmsgHandlerBySampleStream.put(stream, trackPlayerEmsgHandler); diff --git a/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaSource.java b/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaSource.java index 77ddc4ba..70e21e36 100644 --- a/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaSource.java +++ b/app/src/main/java/com/futo/platformplayer/sabr/SabrMediaSource.java @@ -8,8 +8,10 @@ import android.util.SparseArray; import androidx.annotation.Nullable; import androidx.media3.common.C; +import androidx.media3.common.MediaItem; import androidx.media3.common.Timeline; import androidx.media3.common.util.UnstableApi; +import androidx.media3.exoplayer.drm.DrmSessionManagerProvider; import androidx.media3.exoplayer.source.BaseMediaSource; import androidx.media3.exoplayer.source.CompositeSequenceableLoaderFactory; import androidx.media3.exoplayer.source.DefaultCompositeSequenceableLoaderFactory; @@ -34,17 +36,14 @@ import java.io.IOException; @UnstableApi public final class SabrMediaSource extends BaseMediaSource { - /** - * The interval in milliseconds between invocations of {@link - * SourceInfoRefreshListener#onSourceInfoRefreshed(MediaSource, Timeline, Object)} when the - * source's {@link Timeline} is changing dynamically (for example, for incomplete live streams). - */ + private static final int NOTIFY_MANIFEST_INTERVAL_MS = 5000; /** * The minimum default start position for live streams, relative to the start of the live window. */ private static final long MIN_LIVE_DEFAULT_START_POSITION_US = 5000000; private final SabrManifest manifest; + private final MediaItem mediaItem; private final SabrChunkSource.Factory chunkSourceFactory; private final CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; private final LoadErrorHandlingPolicy loadErrorHandlingPolicy; @@ -68,6 +67,7 @@ public final class SabrMediaSource extends BaseMediaSource { private SabrMediaSource( SabrManifest manifest, + MediaItem mediaItem, SabrChunkSource.Factory chunkSourceFactory, CompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory, LoadErrorHandlingPolicy loadErrorHandlingPolicy, @@ -76,6 +76,7 @@ public final class SabrMediaSource extends BaseMediaSource { @Nullable Object tag ) { this.manifest = manifest; + this.mediaItem = mediaItem; this.chunkSourceFactory = chunkSourceFactory; this.compositeSequenceableLoaderFactory = compositeSequenceableLoaderFactory; this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; @@ -87,6 +88,11 @@ public final class SabrMediaSource extends BaseMediaSource { manifestLoadErrorThrower = new ManifestLoadErrorThrower(); } + @Override + public MediaItem getMediaItem() { + return mediaItem; + } + @Override protected void prepareSourceInternal(@Nullable TransferListener mediaTransferListener) { this.mediaTransferListener = mediaTransferListener; @@ -217,7 +223,7 @@ public final class SabrMediaSource extends BaseMediaSource { windowDefaultStartPositionUs, manifest, tag); - refreshSourceInfo(timeline, manifest); + refreshSourceInfo(timeline); } private long getNowUnixTimeUs() { @@ -231,8 +237,10 @@ public final class SabrMediaSource extends BaseMediaSource { public static final class Factory implements MediaSource.Factory { private final SabrChunkSource.Factory chunkSourceFactory; @Nullable private final DataSource.Factory manifestDataSourceFactory; - private final DefaultLoadErrorHandlingPolicy loadErrorHandlingPolicy; + private LoadErrorHandlingPolicy loadErrorHandlingPolicy; private final DefaultCompositeSequenceableLoaderFactory compositeSequenceableLoaderFactory; + @Nullable private DrmSessionManagerProvider drmSessionManagerProvider; + private long livePresentationDelayMs; private boolean livePresentationDelayOverridesManifest; private boolean isCreateCalled; @@ -258,8 +266,35 @@ public final class SabrMediaSource extends BaseMediaSource { } @Override - public MediaSource createMediaSource(Uri uri) { - return null; + public Factory setDrmSessionManagerProvider(DrmSessionManagerProvider drmSessionManagerProvider) { + Assertions.checkState(!isCreateCalled); + this.drmSessionManagerProvider = drmSessionManagerProvider; + return this; + } + + @Override + public SabrMediaSource createMediaSource(MediaItem mediaItem) { + Assertions.checkNotNull(mediaItem); + MediaItem.LocalConfiguration localConfiguration = mediaItem.localConfiguration; + Assertions.checkNotNull(localConfiguration, "MediaItem must have a local configuration"); + Object localTag = localConfiguration.tag; + Assertions.checkArgument( + localTag instanceof SabrManifest, + "MediaItem.localConfiguration.tag must be a SabrManifest" + ); + SabrManifest manifest = (SabrManifest) localTag; + + isCreateCalled = true; + return new SabrMediaSource( + manifest, + mediaItem, + chunkSourceFactory, + compositeSequenceableLoaderFactory, + loadErrorHandlingPolicy, + livePresentationDelayMs, + livePresentationDelayOverridesManifest, + tag + ); } /** @@ -271,8 +306,15 @@ public final class SabrMediaSource extends BaseMediaSource { */ public SabrMediaSource createMediaSource(SabrManifest manifest) { isCreateCalled = true; + + MediaItem mediaItem = new MediaItem.Builder() + .setMediaId("sabr:" + manifest.hashCode()) + .setTag(manifest) + .build(); + return new SabrMediaSource( manifest, + mediaItem, chunkSourceFactory, compositeSequenceableLoaderFactory, loadErrorHandlingPolicy, @@ -301,7 +343,7 @@ public final class SabrMediaSource extends BaseMediaSource { @Override public int[] getSupportedTypes() { - return new int[0]; + return new int[] { C.CONTENT_TYPE_OTHER }; } /** @@ -314,9 +356,10 @@ public final class SabrMediaSource extends BaseMediaSource { * @return This factory, for convenience. * @throws IllegalStateException If one of the {@code create} methods has already been called. */ + @Override public Factory setLoadErrorHandlingPolicy(LoadErrorHandlingPolicy loadErrorHandlingPolicy) { - //Assertions.checkState(!isCreateCalled); - //this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; + Assertions.checkState(!isCreateCalled); + this.loadErrorHandlingPolicy = loadErrorHandlingPolicy; return this; } @@ -338,15 +381,6 @@ public final class SabrMediaSource extends BaseMediaSource { return setLoadErrorHandlingPolicy(new DefaultLoadErrorHandlingPolicy(minLoadableRetryCount)); } - /** - * Sets a tag for the media source which will be published in the {@link - * androidx.media3.Timeline} of the source as {@link - * androidx.media3.Timeline.Window#tag}. - * - * @param tag A tag for the media source. - * @return This factory, for convenience. - * @throws IllegalStateException If one of the {@code create} methods has already been called. - */ public Factory setTag(Object tag) { Assertions.checkState(!isCreateCalled); this.tag = tag; @@ -466,26 +500,32 @@ public final class SabrMediaSource extends BaseMediaSource { @Override public Window getWindow( - int windowIndex, Window window, boolean setTag, long defaultPositionProjectionUs) { + int windowIndex, Window window, long defaultPositionProjectionUs) { Assertions.checkIndex(windowIndex, 0, 1); - long windowDefaultStartPositionUs = getAdjustedWindowDefaultStartPositionUs( - defaultPositionProjectionUs); - Object tag = setTag ? windowTag : null; + + long windowDefaultStartPositionUs = + getAdjustedWindowDefaultStartPositionUs(defaultPositionProjectionUs); + boolean isDynamic = manifest.dynamic && manifest.minUpdatePeriodMs != C.TIME_UNSET && manifest.durationMs == C.TIME_UNSET; + return window.set( - tag, - presentationStartTimeMs, - windowStartTimeMs, + /* uid= */ Window.SINGLE_WINDOW_UID, + /* mediaItem= */ null, + /* manifest= */ manifest, + /* presentationStartTimeMs= */ presentationStartTimeMs, + /* windowStartTimeMs= */ windowStartTimeMs, + /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, /* isSeekable= */ true, - isDynamic, - windowDefaultStartPositionUs, - windowDurationUs, + /* isDynamic= */ isDynamic, + /* liveConfiguration= */ null, + /* defaultPositionUs= */ windowDefaultStartPositionUs, + /* durationUs= */ windowDurationUs, /* firstPeriodIndex= */ 0, /* lastPeriodIndex= */ getPeriodCount() - 1, - offsetInFirstPeriodUs); + /* positionInFirstPeriodUs= */ offsetInFirstPeriodUs); } @Override diff --git a/app/src/main/java/com/futo/platformplayer/sabr/manifest/SabrManifestParser.java b/app/src/main/java/com/futo/platformplayer/sabr/manifest/SabrManifestParser.java index 11f1906e..12ab648d 100644 --- a/app/src/main/java/com/futo/platformplayer/sabr/manifest/SabrManifestParser.java +++ b/app/src/main/java/com/futo/platformplayer/sabr/manifest/SabrManifestParser.java @@ -1,27 +1,30 @@ package com.futo.platformplayer.sabr.manifest; import android.text.TextUtils; +import android.util.Log; import android.util.Pair; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import androidx.media3.common.C; import androidx.media3.common.Format; import androidx.media3.common.DrmInitData; import androidx.media3.common.DrmInitData.SchemeData; + +import com.futo.platformplayer.sabr.ITagUtils; +import com.futo.platformplayer.sabr.MediaFormat; +import com.futo.platformplayer.sabr.MediaFormatComparator; +import com.futo.platformplayer.sabr.MediaFormatUtils; +import com.futo.platformplayer.sabr.MediaItemFormatInfo; +import com.futo.platformplayer.sabr.MediaSubtitle; import com.futo.platformplayer.sabr.manifest.SegmentBase.SegmentList; import com.futo.platformplayer.sabr.manifest.SegmentBase.SegmentTemplate; import com.futo.platformplayer.sabr.manifest.SegmentBase.SegmentTimelineElement; import com.futo.platformplayer.sabr.manifest.SegmentBase.SingleSegmentBase; import androidx.media3.common.MimeTypes; -import com.liskovsoft.mediaserviceinterfaces.data.MediaFormat; -import com.liskovsoft.mediaserviceinterfaces.data.MediaItemFormatInfo; -import com.liskovsoft.mediaserviceinterfaces.data.MediaSubtitle; -import com.liskovsoft.sharedutils.helpers.Helpers; -import com.liskovsoft.sharedutils.mylogger.Log; -import com.liskovsoft.youtubeapi.formatbuilders.mpdbuilder.MediaFormatComparator; -import com.liskovsoft.youtubeapi.formatbuilders.utils.ITagUtils; -import com.liskovsoft.youtubeapi.formatbuilders.utils.MediaFormatUtils; +import androidx.media3.common.util.UnstableApi; import java.util.ArrayList; import java.util.Collections; @@ -31,6 +34,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; +@UnstableApi public class SabrManifestParser { private static final String TAG = SabrManifestParser.class.getSimpleName(); private int mId; @@ -55,6 +59,50 @@ public class SabrManifestParser { return parseSabrManifest(formatInfo); } + public static boolean isInteger(String s) { + return s != null && s.matches("^[-+]?\\d+$"); + } + + public static boolean isNumeric(String s) { + return s != null && s.matches("^[-+]?\\d*\\.?\\d+$"); + } + + public static int parseInt(String numString) { + return parseInt(numString, -1); + } + + public static int parseInt(String numString, int defaultValue) { + if (!isInteger(numString)) { + return defaultValue; + } + + return Integer.parseInt(numString); + } + + public static long parseLong(String numString) { + return parseLong(numString, -1); + } + + public static long parseLong(String numString, long defaultValue) { + if (!isInteger(numString)) { + return defaultValue; + } + + return Long.parseLong(numString); + } + + public static float parseFloat(String numString) { + return parseFloat(numString, -1); + } + + public static float parseFloat(String numString, float defaultValue) { + if (!isNumeric(numString)) { + return defaultValue; + } + + return Float.parseFloat(numString); + } + private SabrManifest parseSabrManifest(MediaItemFormatInfo formatInfo) { long availabilityStartTime = C.TIME_UNSET; long durationMs = getDurationMs(formatInfo); @@ -86,7 +134,7 @@ public class SabrManifestParser { } private static long getDurationMs(MediaItemFormatInfo formatInfo) { - long lenSeconds = Helpers.parseLong(formatInfo.getLengthSeconds()); + long lenSeconds = parseLong(formatInfo.getLengthSeconds()); return lenSeconds > 0 ? lenSeconds * 1_000 : C.TIME_UNSET; } @@ -284,7 +332,7 @@ public class SabrManifestParser { // SegmentURL tag for (String segment : format.getGlobalSegmentList()) { - long duration = Helpers.parseLong(segment, C.TIME_UNSET); + long duration = parseLong(segment, C.TIME_UNSET); int count = 1; for (int i = 0; i < count; i++) { timeline.add(new SegmentTimelineElement(elapsedTime, duration)); @@ -347,14 +395,14 @@ public class SabrManifestParser { int roleFlags = C.ROLE_FLAG_MAIN; int selectionFlags = C.SELECTION_FLAG_DEFAULT; String id = mediaFormat.isDrc() ? mediaFormat.getITag() + "-drc" : mediaFormat.getITag(); - int bandwidth = Helpers.parseInt(mediaFormat.getBitrate(), Format.NO_VALUE); + int bandwidth = parseInt(mediaFormat.getBitrate(), Format.NO_VALUE); String mimeType = MediaFormatUtils.extractMimeType(mediaFormat); String codecs = MediaFormatUtils.extractCodecs(mediaFormat); int width = mediaFormat.getWidth(); int height = mediaFormat.getHeight(); - float frameRate = Helpers.parseFloat(mediaFormat.getFps(), Format.NO_VALUE); + float frameRate = parseFloat(mediaFormat.getFps(), Format.NO_VALUE); int audioChannels = Format.NO_VALUE; - int audioSamplingRate = Helpers.parseInt(ITagUtils.getAudioRateByTag(mediaFormat.getITag()), Format.NO_VALUE); + int audioSamplingRate = parseInt(ITagUtils.getAudioRateByTag(mediaFormat.getITag()), Format.NO_VALUE); String language = mediaFormat.getLanguage(); String baseUrl = mediaFormat.getUrl(); String label = null; @@ -432,22 +480,42 @@ public class SabrManifestParser { protected Representation buildRepresentation( RepresentationInfo representationInfo, - String label, - String extraDrmSchemeType, + @Nullable String label, + @Nullable String extraDrmSchemeType, ArrayList extraDrmSchemeDatas) { - Format format = representationInfo.format; + + // Start from the existing format + Format.Builder formatBuilder = representationInfo.format.buildUpon(); + + // copyWithLabel(label) if (label != null) { - format = format.copyWithLabel(label); + formatBuilder.setLabel(label); } - String drmSchemeType = representationInfo.drmSchemeType != null - ? representationInfo.drmSchemeType : extraDrmSchemeType; + + // Decide scheme type: representationInfo.drmSchemeType wins over extraDrmSchemeType + String drmSchemeType = + representationInfo.drmSchemeType != null + ? representationInfo.drmSchemeType + : extraDrmSchemeType; + + // Accumulate DRM scheme datas (same as your old code) ArrayList drmSchemeDatas = representationInfo.drmSchemeDatas; - drmSchemeDatas.addAll(extraDrmSchemeDatas); + if (extraDrmSchemeDatas != null && !extraDrmSchemeDatas.isEmpty()) { + drmSchemeDatas.addAll(extraDrmSchemeDatas); + } + if (!drmSchemeDatas.isEmpty()) { filterRedundantIncompleteSchemeDatas(drmSchemeDatas); + DrmInitData drmInitData = new DrmInitData(drmSchemeType, drmSchemeDatas); - format = format.copyWithDrmInitData(drmInitData); + + // copyWithDrmInitData(drmInitData) + formatBuilder.setDrmInitData(drmInitData); } + + Format format = formatBuilder.build(); + + // Representation.newInstance(...) still exists with this signature in Media3.:contentReference[oaicite:1]{index=1} return Representation.newInstance( representationInfo.revisionId, format, @@ -455,6 +523,7 @@ public class SabrManifestParser { representationInfo.segmentBase); } + protected Format buildFormat( String id, String containerMimeType, @@ -468,62 +537,44 @@ public class SabrManifestParser { @C.RoleFlags int roleFlags, @C.SelectionFlags int selectionFlags, String codecs) { + String sampleMimeType = getSampleMimeType(containerMimeType, codecs); + + // Base builder: fields common to all track types + Format.Builder builder = new Format.Builder() + .setId(id) + .setContainerMimeType(containerMimeType) + .setSampleMimeType(sampleMimeType) + .setCodecs(codecs) + .setAverageBitrate(bitrate) // same semantics as old "bitrate" arg + .setSelectionFlags(selectionFlags) + .setRoleFlags(roleFlags) + .setLanguage(language); + if (sampleMimeType != null) { if (MimeTypes.isVideo(sampleMimeType)) { - return Format.createVideoContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - width, - height, - frameRate, - /* initializationData= */ null, - selectionFlags, - roleFlags); + // Replacement for createVideoContainerFormat(...) + builder + .setWidth(width) + .setHeight(height) + .setFrameRate(frameRate); + } else if (MimeTypes.isAudio(sampleMimeType)) { - return Format.createAudioContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - /* metadata= */ null, - bitrate, - audioChannels, - audioSamplingRate, - /* initializationData= */ null, - selectionFlags, - roleFlags, - language); + // Replacement for createAudioContainerFormat(...) + builder + .setChannelCount(audioChannels) + .setSampleRate(audioSamplingRate); + } else if (mimeTypeIsRawText(sampleMimeType)) { - return Format.createTextContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - roleFlags, - language, - Format.NO_VALUE); + // Replacement for createTextContainerFormat(...) + // You passed Format.NO_VALUE for accessibilityChannel before, + // which is already the default, but we can be explicit: + builder.setAccessibilityChannel(Format.NO_VALUE); } } - return Format.createContainerFormat( - id, - /* label= */ null, - containerMimeType, - sampleMimeType, - codecs, - bitrate, - selectionFlags, - roleFlags, - language); + + // Replacement for createContainerFormat(...) when no specialized type matched + return builder.build(); } /**