Merge branch 'hls-audio-fixes' into 'master'

HLS audio download

See merge request videostreaming/grayjay!86
This commit is contained in:
Kai DeLorenzo 2025-04-16 17:51:09 +00:00
commit 3b1b288478
4 changed files with 58 additions and 32 deletions

View file

@ -322,7 +322,7 @@ class UISlideOverlays {
val masterPlaylist: HLS.MasterPlaylist
try {
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl, source is IHLSManifestAudioSource)
masterPlaylist.getAudioSources().forEach { it ->

View file

@ -630,10 +630,8 @@ class VideoDownload {
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
suspendCancellableCoroutine { continuation ->
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
val cmd =
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
val statisticsCallback = StatisticsCallback { _ ->
//TODO: Show progress?
@ -643,7 +641,6 @@ class VideoDownload {
val session = FFmpegKit.executeAsync(cmd,
{ session ->
if (ReturnCode.isSuccess(session.returnCode)) {
fileList.delete()
continuation.resumeWith(Result.success(Unit))
} else {
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
@ -651,7 +648,6 @@ class VideoDownload {
} else {
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
}
fileList.delete()
continuation.resumeWithException(RuntimeException(errorMessage))
}
},
@ -1160,6 +1156,8 @@ class VideoDownload {
fun audioContainerToExtension(container: String): String {
if (container.contains("audio/mp4"))
return "mp4a";
else if (container.contains("video/mp4"))
return "mp4";
else if (container.contains("audio/mpeg"))
return "mpga";
else if (container.contains("audio/mp3"))

View file

@ -69,7 +69,7 @@ class VideoExport {
outputFile = f;
} else if (v != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
val f = downloadRoot.createFile(v.container, outputFileName)
val f = downloadRoot.createFile(if (v.container == "application/vnd.apple.mpegurl") "video/mp4" else v.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying video.");
@ -81,8 +81,8 @@ class VideoExport {
outputFile = f;
} else if (a != null) {
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
val f = downloadRoot.createFile(a.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
val f = downloadRoot.createFile(if (a.container == "application/vnd.apple.mpegurl") "video/mp4" else a.container, outputFileName)
?: throw Exception("Failed to create file in external directory.");
Logger.i(TAG, "Copying audio.");

View file

@ -1,5 +1,11 @@
package com.futo.platformplayer.parsers
import android.net.Uri
import androidx.annotation.OptIn
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.hls.playlist.DefaultHlsPlaylistParserFactory
import androidx.media3.exoplayer.hls.playlist.HlsMediaPlaylist
import androidx.media3.exoplayer.hls.playlist.HlsMultivariantPlaylist
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantAudioUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
@ -7,13 +13,20 @@ import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestAudi
import com.futo.platformplayer.api.media.models.streams.sources.IHLSManifestSource
import com.futo.platformplayer.toYesNo
import com.futo.platformplayer.yesNoToBoolean
import java.io.ByteArrayInputStream
import java.net.URI
import java.net.URLConnection
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
class HLS {
companion object {
fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String): MasterPlaylist {
@OptIn(UnstableApi::class)
fun parseMasterPlaylist(masterPlaylistContent: String, sourceUrl: String, isAudioSource: Boolean? = null): MasterPlaylist {
val inputStream = ByteArrayInputStream(masterPlaylistContent.toByteArray())
val playlist = DefaultHlsPlaylistParserFactory().createPlaylistParser()
.parse(Uri.parse(sourceUrl), inputStream)
val baseUrl = URI(sourceUrl).resolve("./").toString()
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
@ -21,27 +34,36 @@ class HLS {
val sessionDataList = mutableListOf<SessionData>()
var independentSegments = false
masterPlaylistContent.lines().forEachIndexed { index, line ->
when {
line.startsWith("#EXT-X-STREAM-INF") -> {
val nextLine = masterPlaylistContent.lines().getOrNull(index + 1)
?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none")
val url = resolveUrl(baseUrl, nextLine)
if (playlist is HlsMediaPlaylist) {
independentSegments = playlist.hasIndependentSegments
if (isAudioSource == true) {
mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))
} else {
variantPlaylists.add(VariantPlaylistReference(playlist.baseUri, StreamInfo(null, null, null, null, null, null, null, null, null)))
}
} else if (playlist is HlsMultivariantPlaylist) {
masterPlaylistContent.lines().forEachIndexed { index, line ->
when {
line.startsWith("#EXT-X-STREAM-INF") -> {
val nextLine = masterPlaylistContent.lines().getOrNull(index + 1)
?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none")
val url = resolveUrl(baseUrl, nextLine)
variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line)))
}
variantPlaylists.add(VariantPlaylistReference(url, parseStreamInfo(line)))
}
line.startsWith("#EXT-X-MEDIA") -> {
mediaRenditions.add(parseMediaRendition(line, baseUrl))
}
line.startsWith("#EXT-X-MEDIA") -> {
mediaRenditions.add(parseMediaRendition(line, baseUrl))
}
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
independentSegments = true
}
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
independentSegments = true
}
line.startsWith("#EXT-X-SESSION-DATA") -> {
val sessionData = parseSessionData(line)
sessionDataList.add(sessionData)
line.startsWith("#EXT-X-SESSION-DATA") -> {
val sessionData = parseSessionData(line)
sessionDataList.add(sessionData)
}
}
}
}
@ -61,7 +83,13 @@ class HLS {
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
val streamInfo = lines.find { it.startsWith("#EXT-X-STREAM-INF:") }?.let { parseStreamInfo(it) }
val initSegment =
lines.find { it.startsWith("#EXT-X-MAP:") }?.substringAfter(":")?.split(",")?.get(0)
?.substringAfter("=")?.trim('"')
val segments = mutableListOf<Segment>()
if (initSegment != null) {
segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment)))
}
var currentSegment: MediaSegment? = null
lines.forEach { line ->
when {
@ -109,10 +137,10 @@ class HLS {
}
}
fun parseAndGetAudioSources(source: Any, content: String, url: String): List<HLSVariantAudioUrlSource> {
fun parseAndGetAudioSources(source: Any, content: String, url: String, isAudioSource: Boolean? = null): List<HLSVariantAudioUrlSource> {
val masterPlaylist: MasterPlaylist
try {
masterPlaylist = parseMasterPlaylist(content, url)
masterPlaylist = parseMasterPlaylist(content, url, isAudioSource)
return masterPlaylist.getAudioSources()
} catch (e: Throwable) {
if (content.lines().any { it.startsWith("#EXTINF:") }) {
@ -270,7 +298,7 @@ class HLS {
val name: String?,
val isDefault: Boolean?,
val isAutoSelect: Boolean?,
val isForced: Boolean?
val isForced: Boolean?,
) {
fun toM3U8Line(): String = buildString {
append("#EXT-X-MEDIA:")
@ -376,7 +404,7 @@ class HLS {
val programDateTime: ZonedDateTime?,
val playlistType: String?,
val streamInfo: StreamInfo?,
val segments: List<Segment>
val segments: List<Segment>,
) {
fun buildM3U8(): String = buildString {
append("#EXTM3U\n")