Skip to content

Commit

Permalink
fix: Handle Livestream Not Started State (#190)
Browse files Browse the repository at this point in the history
- In this commit, we handle the 'Not Started' state for livestreams. If the livestream state is 'Not Started', we display a notice message and, in the background, make requests for the m3u8 URL at 10-second intervals until the request succeeds. Once successful, we reload the player to start the livestream.
  • Loading branch information
PruthiviRaj27 committed Jul 11, 2024
1 parent f94f058 commit 25ce69b
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 5 deletions.
18 changes: 16 additions & 2 deletions player/src/main/java/com/tpstream/player/data/Asset.kt
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ data class Asset(
val currentDateTime = Date()

return when {
liveStream?.status == "Not Started" && liveStream.startTime?.after(currentDateTime) == true ->
liveStream?.noticeMessage != null -> liveStream.noticeMessage
liveStream?.isNotStarted == true && liveStream.startTime?.after(currentDateTime) == true ->
"Live stream is scheduled to start at ${liveStream.getFormattedStartTime()}"
liveStream?.status == "Not Started" ->
liveStream?.isNotStarted == true ->
"Live stream will begin soon."
liveStream?.isEnded == true && liveStream.recordingEnabled && !video.isTranscodingCompleted ->
"Live stream has ended. Recording will be available soon."
Expand Down Expand Up @@ -84,13 +85,26 @@ data class LiveStream(
var recordingEnabled: Boolean,
var enabledDRMForRecording: Boolean,
val enabledDRMForLive: Boolean,
val noticeMessage: String?
) {
val isStreaming: Boolean
get() = status == "Streaming"

val isEnded: Boolean
get() = status == "Completed"

val isNotEnded: Boolean
get() = !isEnded

val isDisconnected: Boolean
get() = status == "Disconnected"

val isNotStarted: Boolean
get() = status == "Not Started"

val isRecording: Boolean
get() = status == "Recording"

fun getFormattedStartTime(): String {
val outputFormat = SimpleDateFormat("MMMM d, yyyy, h:mm a", Locale.getDefault())

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@ internal fun getDomainLiveStream(asset: NetworkAsset): LiveStream? =
startTime = parseDateTime(startTime),
recordingEnabled = recordingEnabled,
enabledDRMForLive = enabledDRMForLive,
enabledDRMForRecording = enabledDRMForRecording
enabledDRMForRecording = enabledDRMForRecording,
noticeMessage = noticeMessage
)
}
} else {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ internal data class NetworkAsset(
val enabledDRMForLive: Boolean,

@SerializedName("enable_drm_for_recording")
val enabledDRMForRecording: Boolean
val enabledDRMForRecording: Boolean,

@SerializedName("notice_message")
val noticeMessage: String?
)
}
41 changes: 41 additions & 0 deletions player/src/main/java/com/tpstream/player/ui/TPStreamPlayerView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ import com.tpstream.player.offline.DownloadTask
import com.tpstream.player.ui.viewmodel.VideoViewModel
import com.tpstream.player.util.ImageSaver
import com.tpstream.player.util.MarkerState
import com.tpstream.player.util.NetworkClient.Companion.makeHeadRequest
import com.tpstream.player.util.getPlayedStatusArray
import kotlinx.coroutines.*
import java.util.*
import java.util.concurrent.TimeUnit

Expand Down Expand Up @@ -56,6 +58,7 @@ class TPStreamPlayerView @JvmOverloads constructor(
private var tPStreamPlayerViewCallBack: TPStreamPlayerViewCallBack? = null
private var noticeScreenLayout: LinearLayout? = null
private var noticeMessage: TextView? = null
private val coroutineScope = CoroutineScope(Dispatchers.IO)

init {
registerDownloadListener()
Expand Down Expand Up @@ -288,18 +291,56 @@ class TPStreamPlayerView @JvmOverloads constructor(
val durationSeparator: TextView = playerView.findViewById(R.id.exo_separator)
val liveLabel: RelativeLayout = playerView.findViewById(R.id.live_label)

noticeScreenLayout?.visibility = View.GONE
durationView.visibility = View.GONE
durationSeparator.visibility = View.GONE
liveLabel.visibility = View.VISIBLE
}

private fun showNoticeScreen(asset: Asset) {
displayNoticeMessage(asset)
handleNotStartedLiveStream(asset)
}

private fun displayNoticeMessage(asset: Asset) {
asset.getNoticeMessage()?.let {
noticeMessage!!.text = it
noticeScreenLayout!!.visibility = View.VISIBLE
}
}

private fun handleNotStartedLiveStream(asset: Asset) {
if (asset.liveStream?.isNotStarted == true) {
asset.liveStream?.url?.let { url ->
coroutineScope.launch {
requestWithRetry(url, 10000)
}
}
}
}

private suspend fun requestWithRetry(url: String, delay: Long) {
while (coroutineScope.isActive) {
try {
val responseCode = makeHeadRequest(url)
if (responseCode == 200) {
withContext(Dispatchers.Main) {
player.load(player.params)
}
return
}
} catch (e: Exception) {
println("Request failed: ${e.message}")
}
delay(delay)
}
}

override fun onDetachedFromWindow() {
super.onDetachedFromWindow()
coroutineScope.cancel()
}

override fun getViewModelStore(): ViewModelStore {
return viewModelStore
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,10 @@ class TpStreamPlayerFragment : Fragment(), DownloadCallback.Listener {

override fun onPlayerError(error: PlaybackException) {
super.onPlayerError(error)
if (isDisconnectedLiveStream(error)){
player.load(player.params)
return
}
val errorPlayerId = SentryLogger.generatePlayerIdString()
SentryLogger.logPlaybackException(error, player?.params, errorPlayerId)
if (error.errorCode == PlaybackException.ERROR_CODE_DRM_LICENSE_ACQUISITION_FAILED) {
Expand Down Expand Up @@ -308,6 +312,13 @@ class TpStreamPlayerFragment : Fragment(), DownloadCallback.Listener {

}

private fun isDisconnectedLiveStream(error: PlaybackException): Boolean {
player.asset?.liveStream?.let {
return it.isNotEnded && error.errorCode == PlaybackException.ERROR_CODE_IO_UNSPECIFIED
}
return false
}

private fun showErrorMessage(message: String) {
viewBinding.errorMessage.visibility = View.VISIBLE
viewBinding.errorMessage.text = message
Expand Down
10 changes: 9 additions & 1 deletion player/src/main/java/com/tpstream/player/util/NetworkClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import java.net.URL
internal class NetworkClient<T : Any>(val klass: Class<T>) {
companion object {
inline operator fun <reified T : Any>invoke() = NetworkClient(T::class.java)

fun makeHeadRequest(url: String): Int {
val request = Request.Builder().head().url(url).build()
OkHttpClient().newCall(request).execute().use { response ->
return response.code
}
}
}

private var client: OkHttpClient = OkHttpClient();
Expand Down Expand Up @@ -74,4 +81,5 @@ internal class NetworkClient<T : Any>(val klass: Class<T>) {
fun onSuccess(result: T)
fun onFailure(exception: TPException)
}
}
}

0 comments on commit 25ce69b

Please sign in to comment.