Further work.

This commit is contained in:
Koen J
2025-12-05 10:24:23 +01:00
parent 618aee9c2c
commit f0569cc7f5
10 changed files with 692 additions and 116 deletions
@@ -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<String> 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<String> 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<List<String>> 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<String> 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";
}
}
@@ -0,0 +1,45 @@
package com.futo.platformplayer.sabr;
import java.util.List;
public interface MediaFormat extends Comparable<MediaFormat> {
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<String> getSegmentUrlList();
List<String> getGlobalSegmentList();
}
@@ -0,0 +1,60 @@
package com.futo.platformplayer.sabr;
import java.util.Comparator;
public class MediaFormatComparator implements Comparator<MediaFormat> {
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);
}
}
@@ -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");
}
}
@@ -0,0 +1,58 @@
package com.futo.platformplayer.sabr;
import java.io.InputStream;
import java.util.List;
public interface MediaItemFormatInfo {
List<MediaFormat> getAdaptiveFormats();
List<MediaFormat> getUrlFormats();
List<MediaSubtitle> 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<InputStream> createMpdStreamObservable();
List<String> createUrlList();
MediaItemStoryboard createStoryboard();
boolean isUnplayable();
boolean isUnknownError();
String getPlayabilityStatus();
boolean isStreamSeekable();
/**
* Stream start time in UTC (!!!).<br/>
* E.g.: <b>2021-10-06T13:36:25+00:00</b>
*/
String getStartTimestamp();
String getUploadDate();
/**
* Stream start time in UNIX format.<br/>
*/
long getStartTimeMs();
/**
* Number of the stream first segment
*/
int getStartSegmentNum();
/**
* Precise segment duration.<br/>
* Used inside live streams
*/
int getSegmentDurationUs();
String getPaidContentText();
}
@@ -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();
}
}
@@ -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);
}
@@ -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<EventStream> 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<TrackGroupArray, TrackGroupInfo[]> 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<SabrChunkSource>[] 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<SabrChunkSource> buildSampleStream(TrackGroupInfo trackGroupInfo, ExoTrackSelection selection, long positionUs) {
private ChunkSampleStream<SabrChunkSource> 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<Format> 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<SabrChunkSource> 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<SabrChunkSource> 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);
@@ -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
@@ -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<SchemeData> 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<SchemeData> 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();
}
/**