diff --git a/app/src/main/java/com/novage/demo/MainActivity.kt b/app/src/main/java/com/novage/demo/MainActivity.kt index b6a6d7d..1b22675 100644 --- a/app/src/main/java/com/novage/demo/MainActivity.kt +++ b/app/src/main/java/com/novage/demo/MainActivity.kt @@ -21,7 +21,7 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - // Enable debugging of WebView to see P2PML logs + // Enable debugging of WebViews WebView.setWebContentsDebuggingEnabled(true) viewModel.setupP2PML() diff --git a/app/src/main/java/com/novage/demo/viewmodel/ExoPlayerViewModel.kt b/app/src/main/java/com/novage/demo/viewmodel/ExoPlayerViewModel.kt index 22d294b..f6f1775 100644 --- a/app/src/main/java/com/novage/demo/viewmodel/ExoPlayerViewModel.kt +++ b/app/src/main/java/com/novage/demo/viewmodel/ExoPlayerViewModel.kt @@ -2,12 +2,18 @@ package com.novage.demo.viewmodel import android.app.Application import android.content.Context +import android.net.Uri import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.viewModelScope import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.common.util.UnstableApi +import androidx.media3.datasource.DataSource +import androidx.media3.datasource.DataSpec +import androidx.media3.datasource.DefaultDataSource +import androidx.media3.datasource.DefaultHttpDataSource +import androidx.media3.datasource.TransferListener import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.hls.HlsMediaSource import com.novage.demo.Streams @@ -15,15 +21,9 @@ import com.novage.p2pml.P2PMediaLoader import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch -import android.net.Uri -import androidx.media3.datasource.DataSource -import androidx.media3.datasource.DataSpec -import androidx.media3.datasource.TransferListener -import androidx.media3.datasource.DefaultDataSource -import androidx.media3.datasource.DefaultHttpDataSource @UnstableApi -class ExoPlayerViewModel(application: Application): AndroidViewModel(application) { +class ExoPlayerViewModel(application: Application) : AndroidViewModel(application) { private val context: Context get() = getApplication() @@ -42,11 +42,12 @@ class ExoPlayerViewModel(application: Application): AndroidViewModel(application coreConfigJson = "{\"swarmId\":\"TEST_KOTLIN\"}", serverPort = 8081, ) - p2pml?.start(context) + p2pml!!.start(context, player) } private fun initializePlayback() { - val manifest = p2pml?.getManifestUrl(Streams.HLS_LIVE_STREAM) ?: throw IllegalStateException("P2PML is not started") + val manifest = p2pml?.getManifestUrl(Streams.HLS_BIG_BUCK_BUNNY) + ?: throw IllegalStateException("P2PML is not started") val loggingDataSourceFactory = LoggingDataSourceFactory(context) val mediaSource = HlsMediaSource.Factory(loggingDataSourceFactory) @@ -65,7 +66,6 @@ class ExoPlayerViewModel(application: Application): AndroidViewModel(application } } }) - p2pml?.attachPlayer(this) } } @@ -100,7 +100,8 @@ class LoggingDataSourceFactory( private val baseDataSourceFactory = DefaultDataSource.Factory(context, httpDataSourceFactory) - override fun createDataSource(): DataSource = LoggingDataSource(baseDataSourceFactory.createDataSource()) + override fun createDataSource(): DataSource = + LoggingDataSource(baseDataSourceFactory.createDataSource()) } diff --git a/p2pml/src/main/java/com/novage/p2pml/DataModels.kt b/p2pml/src/main/java/com/novage/p2pml/DataModels.kt index c1a27bf..1483197 100644 --- a/p2pml/src/main/java/com/novage/p2pml/DataModels.kt +++ b/p2pml/src/main/java/com/novage/p2pml/DataModels.kt @@ -39,8 +39,13 @@ internal data class SegmentRequest( val segmentUrl: String, ) +/** + * Playback info + * @param currentPlayPosition current play position in seconds + * @param currentPlaySpeed current play speed + */ @Serializable -internal data class PlaybackInfo( +data class PlaybackInfo( val currentPlayPosition: Double, val currentPlaySpeed: Float, ) diff --git a/p2pml/src/main/java/com/novage/p2pml/P2PMediaLoader.kt b/p2pml/src/main/java/com/novage/p2pml/P2PMediaLoader.kt index fa56113..99423ee 100644 --- a/p2pml/src/main/java/com/novage/p2pml/P2PMediaLoader.kt +++ b/p2pml/src/main/java/com/novage/p2pml/P2PMediaLoader.kt @@ -9,8 +9,10 @@ import com.novage.p2pml.Constants.QueryParams.MANIFEST import com.novage.p2pml.interop.OnErrorCallback import com.novage.p2pml.interop.P2PReadyCallback import com.novage.p2pml.parser.HlsManifestParser +import com.novage.p2pml.providers.ExoPlayerPlaybackProvider +import com.novage.p2pml.providers.ExternalPlaybackProvider +import com.novage.p2pml.providers.PlaybackProvider import com.novage.p2pml.server.ServerModule -import com.novage.p2pml.utils.ExoPlayerPlaybackCalculator import com.novage.p2pml.utils.P2PStateManager import com.novage.p2pml.utils.Utils import com.novage.p2pml.webview.WebViewManager @@ -57,45 +59,75 @@ class P2PMediaLoader( coreConfigJson, serverPort, emptyList(), - null + null, ) private val engineStateManager = P2PStateManager() - private val playbackCalculator = ExoPlayerPlaybackCalculator() - private val manifestParser = HlsManifestParser(playbackCalculator, serverPort) + private var appState = AppState.INITIALIZED private var job: Job? = null private var scope: CoroutineScope? = null - - private var appState = AppState.INITIALIZED - private var webViewManager: WebViewManager? = null private var serverModule: ServerModule? = null + private var manifestParser: HlsManifestParser? = null + private var webViewManager: WebViewManager? = null + private var playbackProvider: PlaybackProvider? = null + + /** + * Initializes and starts P2P media streaming components. + * + * @param context Android context required for WebView initialization + * @param exoPlayer ExoPlayer instance for media playback + * @throws IllegalStateException if called in an invalid state + */ + fun start( + context: Context, + exoPlayer: ExoPlayer, + ) { + prepareStart(context, ExoPlayerPlaybackProvider(exoPlayer)) + } /** * Initializes and starts P2P media streaming components. * * @param context Android context required for WebView initialization + * @param getPlaybackInfo Function to retrieve playback information * @throws IllegalStateException if called in an invalid state */ - fun start(context: Context) { + fun start( + context: Context, + getPlaybackInfo: () -> PlaybackInfo, + ) { + prepareStart(context, ExternalPlaybackProvider(getPlaybackInfo)) + } + + private fun prepareStart( + context: Context, + provider: PlaybackProvider, + ) { if (appState == AppState.STARTED) { throw IllegalStateException("Cannot start P2PMediaLoader in state: $appState") } job = Job() scope = CoroutineScope(job!! + Dispatchers.Main) + playbackProvider = provider + + initializeComponents(context, provider) - initializeComponents(context) appState = AppState.STARTED } - private fun initializeComponents(context: Context) { + private fun initializeComponents( + context: Context, + playbackProvider: PlaybackProvider, + ) { + manifestParser = HlsManifestParser(playbackProvider, serverPort) webViewManager = WebViewManager( context, scope!!, engineStateManager, - playbackCalculator, + playbackProvider, customJavaScriptInterfaces, onPageLoadFinished = { onWebViewLoaded() }, ) @@ -103,7 +135,7 @@ class P2PMediaLoader( serverModule = ServerModule( webViewManager!!, - manifestParser, + manifestParser!!, engineStateManager, customEngineImplementationPath, onServerStarted = { onServerStarted() }, @@ -121,20 +153,7 @@ class P2PMediaLoader( fun applyDynamicConfig(dynamicCoreConfigJson: String) { ensureStarted() - webViewManager?.applyDynamicConfig(dynamicCoreConfigJson) - } - - /** - * Connects an ExoPlayer instance for playback monitoring. - * Required for P2P segment distribution and synchronization. - * - * @param exoPlayer ExoPlayer instance to monitor - * @throws IllegalStateException if P2PMediaLoader is not started - */ - fun attachPlayer(exoPlayer: ExoPlayer) { - ensureStarted() - - playbackCalculator.setExoPlayer(exoPlayer) + webViewManager!!.applyDynamicConfig(dynamicCoreConfigJson) } /** @@ -175,8 +194,12 @@ class P2PMediaLoader( serverModule?.stop() serverModule = null - playbackCalculator.reset() - manifestParser.reset() + manifestParser?.reset() + manifestParser = null + + playbackProvider?.resetData() + playbackProvider = null + engineStateManager.reset() appState = AppState.STOPPED @@ -188,13 +211,13 @@ class P2PMediaLoader( } private suspend fun onManifestChanged() { - playbackCalculator.resetData() - manifestParser.reset() + playbackProvider!!.resetData() + manifestParser!!.reset() } private fun onWebViewLoaded() { - scope?.launch { - webViewManager?.initCoreEngine(coreConfigJson) + scope!!.launch { + webViewManager!!.initCoreEngine(coreConfigJson) try { readyCallback.onReady() @@ -212,6 +235,6 @@ class P2PMediaLoader( Utils.getUrl(serverPort, CORE_FILE_URL) } - webViewManager?.loadWebView(urlPath) + webViewManager!!.loadWebView(urlPath) } } diff --git a/p2pml/src/main/java/com/novage/p2pml/parser/HlsManifestParser.kt b/p2pml/src/main/java/com/novage/p2pml/parser/HlsManifestParser.kt index ecc2031..ebeb3c2 100644 --- a/p2pml/src/main/java/com/novage/p2pml/parser/HlsManifestParser.kt +++ b/p2pml/src/main/java/com/novage/p2pml/parser/HlsManifestParser.kt @@ -1,7 +1,6 @@ package com.novage.p2pml.parser import androidx.core.net.toUri -import androidx.media3.common.util.Log import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist @@ -11,7 +10,7 @@ import com.novage.p2pml.Constants.StreamTypes import com.novage.p2pml.Segment import com.novage.p2pml.Stream import com.novage.p2pml.UpdateStreamParams -import com.novage.p2pml.utils.ExoPlayerPlaybackCalculator +import com.novage.p2pml.providers.PlaybackProvider import com.novage.p2pml.utils.Utils import io.ktor.http.encodeURLQueryComponent import kotlinx.coroutines.sync.Mutex @@ -21,7 +20,7 @@ import kotlinx.serialization.json.Json @UnstableApi internal class HlsManifestParser( - private val playbackCalculator: ExoPlayerPlaybackCalculator, + private val playbackProvider: PlaybackProvider, private val serverPort: Int, ) { private val parser = HlsPlaylistParser() @@ -120,12 +119,12 @@ internal class HlsManifestParser( return newSegment } - private suspend fun calculateInitialStartTime( + private suspend fun getInitialStartTime( isLive: Boolean, mediaPlaylist: HlsMediaPlaylist, ): Double = if (isLive) { - playbackCalculator.getAbsolutePlaybackPosition(mediaPlaylist) + playbackProvider.getAbsolutePlaybackPosition(mediaPlaylist) } else { 0.0 } @@ -143,7 +142,7 @@ internal class HlsManifestParser( val initializationSegments = mutableSetOf() val segmentsToAdd = mutableListOf() - val initialStartTime = calculateInitialStartTime(isStreamLive, mediaPlaylist) + val initialStartTime = getInitialStartTime(isStreamLive, mediaPlaylist) currentSegmentRuntimeIds.clear() mediaPlaylist.segments.forEachIndexed { index, segment -> if (segment.initializationSegment != null) { @@ -161,10 +160,6 @@ internal class HlsManifestParser( addNewSegment(manifestUrl, segmentIndex, initialStartTime, segment) if (newSegment != null) { segmentsToAdd.add(newSegment) - Log.d( - "SegmentHandler", - "Segment: $segmentIndex - ${newSegment.runtimeId}", - ) } } diff --git a/p2pml/src/main/java/com/novage/p2pml/utils/ExoPlayerPlaybackCalculator.kt b/p2pml/src/main/java/com/novage/p2pml/providers/ExoPlayerPlaybackProvider.kt similarity index 86% rename from p2pml/src/main/java/com/novage/p2pml/utils/ExoPlayerPlaybackCalculator.kt rename to p2pml/src/main/java/com/novage/p2pml/providers/ExoPlayerPlaybackProvider.kt index f4f23a2..9cfab65 100644 --- a/p2pml/src/main/java/com/novage/p2pml/utils/ExoPlayerPlaybackCalculator.kt +++ b/p2pml/src/main/java/com/novage/p2pml/providers/ExoPlayerPlaybackProvider.kt @@ -1,4 +1,4 @@ -package com.novage.p2pml.utils +package com.novage.p2pml.providers import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.ExoPlayer @@ -18,19 +18,15 @@ private data class PlaybackSegment( ) @UnstableApi -internal class ExoPlayerPlaybackCalculator { - private var exoPlayer: ExoPlayer? = null - +internal class ExoPlayerPlaybackProvider( + private val exoPlayer: ExoPlayer, +) : PlaybackProvider { private var currentMediaPlaylist: HlsMediaPlaylist? = null private var currentAbsoluteTime: Double? = null private var currentSegments = mutableMapOf() private val mutex = Mutex() - fun setExoPlayer(exoPlayer: ExoPlayer) { - this.exoPlayer = exoPlayer - } - private fun removeObsoleteSegments(removeUntilId: Long) { val obsoleteIds = currentSegments.keys.filter { it < removeUntilId } @@ -79,7 +75,7 @@ internal class ExoPlayerPlaybackCalculator { ) } - suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double = + override suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double = mutex.withLock { currentMediaPlaylist = parsedMediaPlaylist @@ -101,15 +97,13 @@ internal class ExoPlayerPlaybackCalculator { return@withLock currentAbsoluteTime!! } - suspend fun getPlaybackPositionAndSpeed(): PlaybackInfo = + override suspend fun getPlaybackPositionAndSpeed(): PlaybackInfo = mutex.withLock { - val player = exoPlayer ?: throw IllegalStateException("ExoPlayer instance is null") - val playbackPositionInSeconds = withContext(Dispatchers.Main) { - player.currentPosition / 1000.0 + exoPlayer.currentPosition / 1000.0 } - val playbackSpeed = withContext(Dispatchers.Main) { player.playbackParameters.speed } + val playbackSpeed = withContext(Dispatchers.Main) { exoPlayer.playbackParameters.speed } if (currentMediaPlaylist == null || currentMediaPlaylist?.hasEndTag == true) { return PlaybackInfo(playbackPositionInSeconds, playbackSpeed) @@ -133,15 +127,10 @@ internal class ExoPlayerPlaybackCalculator { return PlaybackInfo(segmentAbsolutePlayTime, playbackSpeed) } - suspend fun resetData() = + override suspend fun resetData() = mutex.withLock { currentSegments.clear() currentMediaPlaylist = null currentAbsoluteTime = null } - - suspend fun reset() { - resetData() - exoPlayer = null - } } diff --git a/p2pml/src/main/java/com/novage/p2pml/providers/ExternalPlaybackProvider.kt b/p2pml/src/main/java/com/novage/p2pml/providers/ExternalPlaybackProvider.kt new file mode 100644 index 0000000..44fcc57 --- /dev/null +++ b/p2pml/src/main/java/com/novage/p2pml/providers/ExternalPlaybackProvider.kt @@ -0,0 +1,26 @@ +package com.novage.p2pml.providers + +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist +import com.novage.p2pml.PlaybackInfo +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class ExternalPlaybackProvider( + private val getPlaybackInfo: () -> PlaybackInfo, +) : PlaybackProvider { + @OptIn(UnstableApi::class) + override suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double = + withContext(Dispatchers.Main) { + return@withContext getPlaybackInfo().currentPlayPosition + } + + override suspend fun getPlaybackPositionAndSpeed(): PlaybackInfo = + withContext(Dispatchers.Main) { + return@withContext getPlaybackInfo() + } + + override suspend fun resetData() { + } +} diff --git a/p2pml/src/main/java/com/novage/p2pml/providers/PlaybackProvider.kt b/p2pml/src/main/java/com/novage/p2pml/providers/PlaybackProvider.kt new file mode 100644 index 0000000..7c23062 --- /dev/null +++ b/p2pml/src/main/java/com/novage/p2pml/providers/PlaybackProvider.kt @@ -0,0 +1,15 @@ +package com.novage.p2pml.providers + +import androidx.annotation.OptIn +import androidx.media3.common.util.UnstableApi +import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist +import com.novage.p2pml.PlaybackInfo + +interface PlaybackProvider { + @OptIn(UnstableApi::class) + suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double + + suspend fun getPlaybackPositionAndSpeed(): PlaybackInfo + + suspend fun resetData() +} diff --git a/p2pml/src/main/java/com/novage/p2pml/webview/WebViewManager.kt b/p2pml/src/main/java/com/novage/p2pml/webview/WebViewManager.kt index 1d4bcfa..420cb79 100644 --- a/p2pml/src/main/java/com/novage/p2pml/webview/WebViewManager.kt +++ b/p2pml/src/main/java/com/novage/p2pml/webview/WebViewManager.kt @@ -10,7 +10,7 @@ import androidx.annotation.OptIn import androidx.media3.common.util.UnstableApi import androidx.webkit.WebViewClientCompat import com.novage.p2pml.DynamicP2PCoreConfig -import com.novage.p2pml.utils.ExoPlayerPlaybackCalculator +import com.novage.p2pml.providers.PlaybackProvider import com.novage.p2pml.utils.P2PStateManager import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CoroutineScope @@ -29,9 +29,9 @@ internal class WebViewManager( context: Context, private val coroutineScope: CoroutineScope, private val engineStateManager: P2PStateManager, - private val playbackCalculator: ExoPlayerPlaybackCalculator, + private val playbackProvider: PlaybackProvider, customJavaScriptInterfaces: List>, - private val onPageLoadFinished: () -> Unit, + onPageLoadFinished: () -> Unit, ) { @SuppressLint("SetJavaScriptEnabled") private val webView = @@ -82,7 +82,7 @@ internal class WebViewManager( } val currentPlaybackInfo = - playbackCalculator.getPlaybackPositionAndSpeed() + playbackProvider.getPlaybackPositionAndSpeed() val playbackInfoJson = Json.encodeToString(currentPlaybackInfo) sendPlaybackInfo(playbackInfoJson)