diff --git a/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java b/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java index c2710f42a08..2fe1ae08e3b 100644 --- a/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java +++ b/demos/main/src/main/java/androidx/media3/demo/main/IntentUtil.java @@ -26,6 +26,7 @@ import androidx.media3.common.C; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem.ClippingConfiguration; +import androidx.media3.common.MediaItem.LiveConfiguration; import androidx.media3.common.MediaItem.SubtitleConfiguration; import androidx.media3.common.MediaMetadata; import androidx.media3.common.Player; @@ -86,6 +87,9 @@ public class IntentUtil { } } + public static final String LIVE_OFFSET_TARGET_VALUE = "live_offset_target"; + public static final String LIVE_OFFSET_ADJUSTMENT_SPEED = "live_offset_adjust_speed"; + /** Creates a list of {@link MediaItem media items} from an {@link Intent}. */ public static List createMediaItemsFromIntent(Intent intent) { List mediaItems = new ArrayList<>(); @@ -143,6 +147,9 @@ private static MediaItem createMediaItemFromIntent( SubtitleConfiguration subtitleConfiguration = createSubtitleConfiguration(intent, extrasKeySuffix); long imageDurationMs = intent.getLongExtra(IMAGE_DURATION_MS + extrasKeySuffix, C.TIME_UNSET); + @Nullable + LiveConfiguration liveConfiguration = createLiveConfiguration(intent, extrasKeySuffix); + MediaItem.Builder builder = new MediaItem.Builder() .setUri(uri) @@ -165,9 +172,37 @@ private static MediaItem createMediaItemFromIntent( builder.setSubtitleConfigurations(ImmutableList.of(subtitleConfiguration)); } + if (liveConfiguration != null) { + builder.setLiveConfiguration(liveConfiguration); + } + return populateDrmPropertiesFromIntent(builder, intent, extrasKeySuffix).build(); } + @Nullable + private static LiveConfiguration createLiveConfiguration(Intent intent, String extrasKeySuffix) { + LiveConfiguration.Builder builder = null; + boolean fast_resync = intent.hasExtra(LIVE_OFFSET_ADJUSTMENT_SPEED + extrasKeySuffix); + if (fast_resync) { + float resyncPercentChange = intent.getFloatExtra(LIVE_OFFSET_ADJUSTMENT_SPEED, 0.0f) / 100.0f; + builder = new LiveConfiguration.Builder(); + builder + .setMaxPlaybackSpeed(1.0f + resyncPercentChange) + .setMinPlaybackSpeed(1.0f - resyncPercentChange); + } + + if (intent.hasExtra(LIVE_OFFSET_TARGET_VALUE)) { + int liveTargetOffsetMs = intent.getIntExtra(LIVE_OFFSET_TARGET_VALUE, 0) * 1000; + builder = builder == null ? new LiveConfiguration.Builder() : builder; + + builder.setTargetOffsetMs(liveTargetOffsetMs); + if (fast_resync) { + builder.setMinOffsetMs(liveTargetOffsetMs).setMinOffsetMs(liveTargetOffsetMs); + } + } + return builder == null ? null : builder.build(); + } + @Nullable private static MediaItem.SubtitleConfiguration createSubtitleConfiguration( Intent intent, String extrasKeySuffix) { diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java index ccad73c1f7f..843e319bfc5 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/HlsMediaSource.java @@ -57,6 +57,7 @@ import androidx.media3.exoplayer.upstream.CmcdConfiguration; import androidx.media3.exoplayer.upstream.DefaultLoadErrorHandlingPolicy; import androidx.media3.exoplayer.upstream.LoadErrorHandlingPolicy; +import androidx.media3.exoplayer.util.SntpClient; import androidx.media3.extractor.Extractor; import androidx.media3.extractor.text.SubtitleParser; import com.google.errorprone.annotations.CanIgnoreReturnValue; @@ -598,7 +599,7 @@ private SinglePeriodTimeline createTimelineForLive( return new SinglePeriodTimeline( presentationStartTimeMs, windowStartTimeMs, - /* elapsedRealtimeEpochOffsetMs= */ C.TIME_UNSET, + SntpClient.getElapsedRealtimeOffsetMs(), periodDurationUs, /* windowDurationUs= */ playlist.durationUs, /* windowPositionInPeriodUs= */ offsetFromInitialStartTimeUs, diff --git a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java index 9e2d786d186..8bde386136d 100644 --- a/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java +++ b/libraries/exoplayer_hls/src/main/java/androidx/media3/exoplayer/hls/playlist/DefaultHlsPlaylistTracker.java @@ -19,6 +19,7 @@ import static androidx.media3.common.util.Util.castNonNull; import static java.lang.Math.max; +import android.annotation.SuppressLint; import android.net.Uri; import android.os.Handler; import android.os.SystemClock; @@ -26,6 +27,7 @@ import androidx.media3.common.C; import androidx.media3.common.ParserException; import androidx.media3.common.util.Assertions; +import androidx.media3.common.util.Log; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; import androidx.media3.datasource.DataSource; @@ -43,6 +45,7 @@ import androidx.media3.exoplayer.upstream.Loader; import androidx.media3.exoplayer.upstream.Loader.LoadErrorAction; import androidx.media3.exoplayer.upstream.ParsingLoadable; +import androidx.media3.exoplayer.util.SntpClient; import com.google.common.collect.Iterables; import java.io.IOException; import java.util.HashMap; @@ -54,6 +57,8 @@ public final class DefaultHlsPlaylistTracker implements HlsPlaylistTracker, Loader.Callback> { + public static final String TAG = "DefaultHlsPlaylistTracker"; + /** Factory for {@link DefaultHlsPlaylistTracker} instances. */ public static final Factory FACTORY = DefaultHlsPlaylistTracker::new; @@ -141,6 +146,25 @@ public void start( playlistParserFactory.createPlaylistParser()); Assertions.checkState(initialPlaylistLoader == null); initialPlaylistLoader = new Loader("DefaultHlsPlaylistTracker:MultivariantPlaylist"); + SntpClient.InitializationCallback callback = + new SntpClient.InitializationCallback() { + @Override + public void onInitialized() { + requestMasterPlaylist(eventDispatcher, multivariantPlaylistLoadable); + } + + @SuppressLint("Range") + @Override + public void onInitializationFailed(IOException error) { + Log.w(TAG, "NTP time init failed, use default system time", error); + requestMasterPlaylist(eventDispatcher, multivariantPlaylistLoadable); + } + }; + SntpClient.initialize(initialPlaylistLoader, callback); + } + + private void requestMasterPlaylist( + EventDispatcher eventDispatcher, ParsingLoadable multivariantPlaylistLoadable) { long elapsedRealtime = initialPlaylistLoader.startLoading( multivariantPlaylistLoadable, @@ -728,6 +752,27 @@ private void loadPlaylistInternal(Uri playlistRequestUri) { } private void loadPlaylistImmediately(Uri playlistRequestUri) { + if (primaryMediaPlaylistUrl == null || playlistRequestUri.equals(primaryMediaPlaylistUrl)) { + SntpClient.InitializationCallback callback = + new SntpClient.InitializationCallback() { + @Override + public void onInitialized() { + loadAfterTimeSync(playlistRequestUri); + } + + @Override + public void onInitializationFailed(IOException error) { + Log.w(TAG, "NTP time init failed, use default system time", error); + loadAfterTimeSync(playlistRequestUri); + } + }; + SntpClient.initialize(mediaPlaylistLoader, callback); + } else { + loadAfterTimeSync(playlistRequestUri); + } + } + + private void loadAfterTimeSync(Uri playlistRequestUri) { ParsingLoadable.Parser mediaPlaylistParser = playlistParserFactory.createPlaylistParser(multivariantPlaylist, playlistSnapshot); ParsingLoadable mediaPlaylistLoadable =