Skip to content

Commit

Permalink
Merge pull request #1 from Novage/refactor/playback-calculator
Browse files Browse the repository at this point in the history
Feat: add support for different players
  • Loading branch information
DimaDemchenko authored Jan 15, 2025
2 parents 0b4eb6f + 295e03c commit 2e4fa53
Show file tree
Hide file tree
Showing 9 changed files with 134 additions and 80 deletions.
2 changes: 1 addition & 1 deletion app/src/main/java/com/novage/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
23 changes: 12 additions & 11 deletions app/src/main/java/com/novage/demo/viewmodel/ExoPlayerViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,28 @@ 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
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()

Expand All @@ -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)
Expand All @@ -65,7 +66,6 @@ class ExoPlayerViewModel(application: Application): AndroidViewModel(application
}
}
})
p2pml?.attachPlayer(this)
}
}

Expand Down Expand Up @@ -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())
}


Expand Down
7 changes: 6 additions & 1 deletion p2pml/src/main/java/com/novage/p2pml/DataModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down
89 changes: 56 additions & 33 deletions p2pml/src/main/java/com/novage/p2pml/P2PMediaLoader.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -57,53 +59,83 @@ 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() },
)

serverModule =
ServerModule(
webViewManager!!,
manifestParser,
manifestParser!!,
engineStateManager,
customEngineImplementationPath,
onServerStarted = { onServerStarted() },
Expand All @@ -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)
}

/**
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -212,6 +235,6 @@ class P2PMediaLoader(
Utils.getUrl(serverPort, CORE_FILE_URL)
}

webViewManager?.loadWebView(urlPath)
webViewManager!!.loadWebView(urlPath)
}
}
15 changes: 5 additions & 10 deletions p2pml/src/main/java/com/novage/p2pml/parser/HlsManifestParser.kt
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -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()
Expand Down Expand Up @@ -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
}
Expand All @@ -143,7 +142,7 @@ internal class HlsManifestParser(
val initializationSegments = mutableSetOf<HlsMediaPlaylist.Segment>()
val segmentsToAdd = mutableListOf<Segment>()

val initialStartTime = calculateInitialStartTime(isStreamLive, mediaPlaylist)
val initialStartTime = getInitialStartTime(isStreamLive, mediaPlaylist)
currentSegmentRuntimeIds.clear()
mediaPlaylist.segments.forEachIndexed { index, segment ->
if (segment.initializationSegment != null) {
Expand All @@ -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}",
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<Long, PlaybackSegment>()
private val mutex = Mutex()

fun setExoPlayer(exoPlayer: ExoPlayer) {
this.exoPlayer = exoPlayer
}

private fun removeObsoleteSegments(removeUntilId: Long) {
val obsoleteIds = currentSegments.keys.filter { it < removeUntilId }

Expand Down Expand Up @@ -79,7 +75,7 @@ internal class ExoPlayerPlaybackCalculator {
)
}

suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double =
override suspend fun getAbsolutePlaybackPosition(parsedMediaPlaylist: HlsMediaPlaylist): Double =
mutex.withLock {
currentMediaPlaylist = parsedMediaPlaylist

Expand All @@ -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)
Expand All @@ -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
}
}
Loading

0 comments on commit 2e4fa53

Please sign in to comment.