Merge branch 'master' of gitlab.futo.org:videostreaming/grayjay

This commit is contained in:
Kelvin 2023-11-23 17:28:23 +01:00
commit eb3dd854d4
3 changed files with 92 additions and 47 deletions

View file

@ -69,7 +69,7 @@ class ChromecastCastingDevice : CastingDevice {
return; return;
} }
Logger.i(FastCastCastingDevice.TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)"); Logger.i(TAG, "Start streaming (streamType: $streamType, contentType: $contentType, contentId: $contentId, resumePosition: $resumePosition, duration: $duration)");
time = resumePosition; time = resumePosition;
_streamType = streamType; _streamType = streamType;

View file

@ -336,21 +336,23 @@ class StateCasting {
if (sourceCount > 1) { if (sourceCount > 1) {
if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) { if (videoSource is LocalVideoSource || audioSource is LocalAudioSource || subtitleSource is LocalSubtitleSource) {
if (ad is AirPlayCastingDevice) { if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as local HLS");
castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition); castLocalHls(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition);
} else { } else {
Logger.i(TAG, "Casting as local DASH");
castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition); castLocalDash(video, videoSource as LocalVideoSource?, audioSource as LocalAudioSource?, subtitleSource as LocalSubtitleSource?, resumePosition);
} }
} else { } else {
StateApp.instance.scope.launch(Dispatchers.IO) { StateApp.instance.scope.launch(Dispatchers.IO) {
try { try {
if (ad is FastCastCastingDevice) { if (ad is FastCastCastingDevice) {
Logger.i(TAG, "Casting as dash direct"); Logger.i(TAG, "Casting as DASH direct");
castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castDashDirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
} else if (ad is AirPlayCastingDevice) { } else if (ad is AirPlayCastingDevice) {
Logger.i(TAG, "Casting as HLS indirect"); Logger.i(TAG, "Casting as HLS indirect");
castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castHlsIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
} else { } else {
Logger.i(TAG, "Casting as dash indirect"); Logger.i(TAG, "Casting as DASH indirect");
castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition); castDashIndirect(contentResolver, video, videoSource as IVideoUrlSource?, audioSource as IAudioUrlSource?, subtitleSource, resumePosition);
} }
} catch (e: Throwable) { } catch (e: Throwable) {
@ -498,8 +500,8 @@ class StateCasting {
val duration = videoSource.duration val duration = videoSource.duration
val videoVariantPlaylistPath = "/video-playlist-${id}" val videoVariantPlaylistPath = "/video-playlist-${id}"
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
val videoVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), videoUrl)) val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, videoVariantPlaylistSegments) val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, videoVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
@ -508,7 +510,7 @@ class StateCasting {
).withTag("castLocalHls") ).withTag("castLocalHls")
variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo( variantPlaylistReferences.add(HLS.VariantPlaylistReference(videoVariantPlaylistUrl, HLS.StreamInfo(
videoSource.bitrate, "${videoSource.width}x${videoSource.height}", videoSource.codec, null, null, if (audioSource != null) "audio" else null, if (subtitleSource != null) "subtitles" else null, null))) videoSource.bitrate, "${videoSource.width}x${videoSource.height}", videoSource.codec, null, null, if (audioSource != null) "audio" else null, if (subtitleSource != null) "subtitles" else null, null, null)))
} }
if (audioSource != null) { if (audioSource != null) {
@ -520,8 +522,8 @@ class StateCasting {
val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown")
val audioVariantPlaylistPath = "/audio-playlist-${id}" val audioVariantPlaylistPath = "/audio-playlist-${id}"
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
val audioVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), audioUrl)) val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, audioVariantPlaylistSegments) val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, audioVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
@ -541,8 +543,8 @@ class StateCasting {
val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown") val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown")
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}" val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
val subtitleVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), subtitleUrl)) val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitleUrl))
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, subtitleVariantPlaylistSegments) val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, subtitleVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
@ -704,7 +706,7 @@ class StateCasting {
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, variantPlaylistRef.url) val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, variantPlaylistRef.url)
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant") }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant")
@ -716,17 +718,19 @@ class StateCasting {
} }
for (mediaRendition in masterPlaylist.mediaRenditions) { for (mediaRendition in masterPlaylist.mediaRenditions) {
val playlistId = UUID.randomUUID(); val playlistId = UUID.randomUUID()
val newPlaylistPath = "/hls-playlist-${playlistId}"
val newPlaylistUrl = url + newPlaylistPath;
var newPlaylistUrl: String? = null
if (mediaRendition.uri != null) { if (mediaRendition.uri != null) {
val newPlaylistPath = "/hls-playlist-${playlistId}"
newPlaylistUrl = url + newPlaylistPath
_castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext -> _castServer.addHandler(HttpFuntionHandler("GET", newPlaylistPath) { vpContext ->
val vpHeaders = vpContext.headers.clone() val vpHeaders = vpContext.headers.clone()
vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl"; vpHeaders["Content-Type"] = "application/vnd.apple.mpegurl";
val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri) val variantPlaylist = HLS.downloadAndParseVariantPlaylist(_client, mediaRendition.uri)
val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist) val proxiedVariantPlaylist = proxyVariantPlaylist(url, playlistId, variantPlaylist, video.isLive)
val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8() val proxiedVariantPlaylist_m3u8 = proxiedVariantPlaylist.buildM3U8()
vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8); vpContext.respondCode(200, vpHeaders, proxiedVariantPlaylist_m3u8);
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant") }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsVariant")
@ -748,12 +752,15 @@ class StateCasting {
}.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsMaster") }.withHeader("Access-Control-Allow-Origin", "*"), true).withTag("castProxiedHlsMaster")
Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath)."); Logger.i(TAG, "added new castHlsIndirect handlers (hlsPath: $hlsPath).");
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, resumePosition, video.duration.toDouble());
//ChromeCast is sometimes funky with resume position 0
val hackfixResumePosition = if (ad is ChromecastCastingDevice && !video.isLive && resumePosition == 0.0) 1.0 else resumePosition;
ad.loadVideo(if (video.isLive) "LIVE" else "BUFFERED", "application/vnd.apple.mpegurl", hlsUrl, hackfixResumePosition, video.duration.toDouble());
return listOf(hlsUrl); return listOf(hlsUrl);
} }
private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, proxySegments: Boolean = true): HLS.VariantPlaylist { private fun proxyVariantPlaylist(url: String, playlistId: UUID, variantPlaylist: HLS.VariantPlaylist, isLive: Boolean, proxySegments: Boolean = true): HLS.VariantPlaylist {
val newSegments = arrayListOf<HLS.Segment>() val newSegments = arrayListOf<HLS.Segment>()
if (proxySegments) { if (proxySegments) {
@ -771,26 +778,31 @@ class StateCasting {
variantPlaylist.mediaSequence, variantPlaylist.mediaSequence,
variantPlaylist.discontinuitySequence, variantPlaylist.discontinuitySequence,
variantPlaylist.programDateTime, variantPlaylist.programDateTime,
variantPlaylist.playlistType,
newSegments newSegments
) )
} }
private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment { private fun proxySegment(url: String, playlistId: UUID, segment: HLS.Segment, index: Long): HLS.Segment {
val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}" if (segment is HLS.MediaSegment) {
val newSegmentUrl = url + newSegmentPath; val newSegmentPath = "/hls-playlist-${playlistId}-segment-${index}"
val newSegmentUrl = url + newSegmentPath;
if (_castServer.getHandler("GET", newSegmentPath) == null) { if (_castServer.getHandler("GET", newSegmentPath) == null) {
_castServer.addHandler( _castServer.addHandler(
HttpProxyHandler("GET", newSegmentPath, segment.uri, true) HttpProxyHandler("GET", newSegmentPath, segment.uri, true)
.withInjectedHost() .withInjectedHost()
.withHeader("Access-Control-Allow-Origin", "*"), true .withHeader("Access-Control-Allow-Origin", "*"), true
).withTag("castProxiedHlsVariant") ).withTag("castProxiedHlsVariant")
}
return HLS.MediaSegment(
segment.duration,
newSegmentUrl
)
} else {
return segment
} }
return HLS.Segment(
segment.duration,
newSegmentUrl
)
} }
private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> { private suspend fun castHlsIndirect(contentResolver: ContentResolver, video: IPlatformVideoDetails, videoSource: IVideoUrlSource?, audioSource: IAudioUrlSource?, subtitleSource: ISubtitleSource?, resumePosition: Double) : List<String> {
@ -813,8 +825,8 @@ class StateCasting {
val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown") val duration = audioSource.duration ?: videoSource?.duration ?: throw Exception("Duration unknown")
val audioVariantPlaylistPath = "/audio-playlist-${id}" val audioVariantPlaylistPath = "/audio-playlist-${id}"
val audioVariantPlaylistUrl = url + audioVariantPlaylistPath val audioVariantPlaylistUrl = url + audioVariantPlaylistPath
val audioVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), audioUrl)) val audioVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), audioUrl))
val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, audioVariantPlaylistSegments) val audioVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, audioVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", audioVariantPlaylistPath, audioVariantPlaylist.buildM3U8(),
@ -866,8 +878,8 @@ class StateCasting {
val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown") val duration = videoSource?.duration ?: audioSource?.duration ?: throw Exception("Duration unknown")
val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}" val subtitleVariantPlaylistPath = "/subtitle-playlist-${id}"
val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath val subtitleVariantPlaylistUrl = url + subtitleVariantPlaylistPath
val subtitleVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), subtitlesUrl)) val subtitleVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), subtitlesUrl))
val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, subtitleVariantPlaylistSegments) val subtitleVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, subtitleVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", subtitleVariantPlaylistPath, subtitleVariantPlaylist.buildM3U8(),
@ -886,8 +898,8 @@ class StateCasting {
val duration = videoSource.duration val duration = videoSource.duration
val videoVariantPlaylistPath = "/video-playlist-${id}" val videoVariantPlaylistPath = "/video-playlist-${id}"
val videoVariantPlaylistUrl = url + videoVariantPlaylistPath val videoVariantPlaylistUrl = url + videoVariantPlaylistPath
val videoVariantPlaylistSegments = listOf(HLS.Segment(duration.toDouble(), videoUrl)) val videoVariantPlaylistSegments = listOf(HLS.MediaSegment(duration.toDouble(), videoUrl))
val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, videoVariantPlaylistSegments) val videoVariantPlaylist = HLS.VariantPlaylist(3, duration.toInt(), 0, 0, null, null, videoVariantPlaylistSegments)
_castServer.addHandler( _castServer.addHandler(
HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(), HttpConstantHandler("GET", videoVariantPlaylistPath, videoVariantPlaylist.buildM3U8(),
@ -904,7 +916,7 @@ class StateCasting {
null, null,
if (audioSource != null) "audio" else null, if (audioSource != null) "audio" else null,
if (subtitleSource != null) "subtitles" else null, if (subtitleSource != null) "subtitles" else null,
null))) null, null)))
_castServer.addHandler( _castServer.addHandler(
HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true) HttpProxyHandler("GET", videoPath, videoSource.getVideoUrl(), true)

View file

@ -66,18 +66,22 @@ class HLS {
val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let { val programDateTime = lines.find { it.startsWith("#EXT-X-PROGRAM-DATE-TIME:") }?.substringAfter(":")?.let {
ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME) ZonedDateTime.parse(it, DateTimeFormatter.ISO_DATE_TIME)
} }
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
val segments = mutableListOf<Segment>() val segments = mutableListOf<Segment>()
var currentSegment: Segment? = null var currentSegment: MediaSegment? = null
lines.forEach { line -> lines.forEach { line ->
when { when {
line.startsWith("#EXTINF:") -> { line.startsWith("#EXTINF:") -> {
val duration = line.substringAfter(":").substringBefore(",").toDoubleOrNull() val duration = line.substringAfter(":").substringBefore(",").toDoubleOrNull()
?: throw Exception("Invalid segment duration format") ?: throw Exception("Invalid segment duration format")
currentSegment = Segment(duration = duration) currentSegment = MediaSegment(duration = duration)
} }
line.startsWith("#") -> { line == "#EXT-X-DISCONTINUITY" -> {
// Handle other tags if necessary segments.add(DiscontinuitySegment())
}
line =="#EXT-X-ENDLIST" -> {
segments.add(EndListSegment())
} }
else -> { else -> {
currentSegment?.let { currentSegment?.let {
@ -89,7 +93,7 @@ class HLS {
} }
} }
return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, segments) return VariantPlaylist(version, targetDuration, mediaSequence, discontinuitySequence, programDateTime, playlistType, segments)
} }
private fun resolveUrl(baseUrl: String, url: String): String { private fun resolveUrl(baseUrl: String, url: String): String {
@ -113,6 +117,7 @@ class HLS {
frameRate = attributes["FRAME-RATE"], frameRate = attributes["FRAME-RATE"],
videoRange = attributes["VIDEO-RANGE"], videoRange = attributes["VIDEO-RANGE"],
audio = attributes["AUDIO"], audio = attributes["AUDIO"],
video = attributes["VIDEO"],
subtitles = attributes["SUBTITLES"], subtitles = attributes["SUBTITLES"],
closedCaptions = attributes["CLOSED-CAPTIONS"] closedCaptions = attributes["CLOSED-CAPTIONS"]
) )
@ -159,7 +164,7 @@ class HLS {
return attributes return attributes
} }
private val _quoteList = listOf("GROUP-ID", "NAME", "URI", "CODECS", "AUDIO") private val _quoteList = listOf("GROUP-ID", "NAME", "URI", "CODECS", "AUDIO", "VIDEO")
private fun shouldQuote(key: String, value: String?): Boolean { private fun shouldQuote(key: String, value: String?): Boolean {
if (value == null) if (value == null)
return false; return false;
@ -200,6 +205,7 @@ class HLS {
val frameRate: String?, val frameRate: String?,
val videoRange: String?, val videoRange: String?,
val audio: String?, val audio: String?,
val video: String?,
val subtitles: String?, val subtitles: String?,
val closedCaptions: String? val closedCaptions: String?
) )
@ -270,6 +276,7 @@ class HLS {
"FRAME-RATE" to streamInfo.frameRate, "FRAME-RATE" to streamInfo.frameRate,
"VIDEO-RANGE" to streamInfo.videoRange, "VIDEO-RANGE" to streamInfo.videoRange,
"AUDIO" to streamInfo.audio, "AUDIO" to streamInfo.audio,
"VIDEO" to streamInfo.video,
"SUBTITLES" to streamInfo.subtitles, "SUBTITLES" to streamInfo.subtitles,
"CLOSED-CAPTIONS" to streamInfo.closedCaptions "CLOSED-CAPTIONS" to streamInfo.closedCaptions
) )
@ -283,6 +290,7 @@ class HLS {
val mediaSequence: Long, val mediaSequence: Long,
val discontinuitySequence: Int, val discontinuitySequence: Int,
val programDateTime: ZonedDateTime?, val programDateTime: ZonedDateTime?,
val playlistType: String?,
val segments: List<Segment> val segments: List<Segment>
) { ) {
fun buildM3U8(): String = buildString { fun buildM3U8(): String = buildString {
@ -291,19 +299,44 @@ class HLS {
append("#EXT-X-TARGETDURATION:$targetDuration\n") append("#EXT-X-TARGETDURATION:$targetDuration\n")
append("#EXT-X-MEDIA-SEQUENCE:$mediaSequence\n") append("#EXT-X-MEDIA-SEQUENCE:$mediaSequence\n")
append("#EXT-X-DISCONTINUITY-SEQUENCE:$discontinuitySequence\n") append("#EXT-X-DISCONTINUITY-SEQUENCE:$discontinuitySequence\n")
playlistType?.let {
append("#EXT-X-PLAYLIST-TYPE:$it\n")
}
programDateTime?.let { programDateTime?.let {
append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n") append("#EXT-X-PROGRAM-DATE-TIME:${it.format(DateTimeFormatter.ISO_DATE_TIME)}\n")
} }
segments.forEach { segment -> segments.forEach { segment ->
append("#EXTINF:${segment.duration},\n") append(segment.toM3U8Line())
append(segment.uri + "\n")
} }
} }
} }
data class Segment( abstract class Segment {
abstract fun toM3U8Line(): String
}
data class MediaSegment (
val duration: Double, val duration: Double,
var uri: String = "" var uri: String = ""
) ) : Segment() {
override fun toM3U8Line(): String = buildString {
append("#EXTINF:${duration},\n")
append(uri + "\n")
}
}
class DiscontinuitySegment : Segment() {
override fun toM3U8Line(): String = buildString {
append("#EXT-X-DISCONTINUITY\n")
}
}
class EndListSegment : Segment() {
override fun toM3U8Line(): String = buildString {
append("#EXT-X-ENDLIST\n")
}
}
} }