mirror of
https://gitlab.futo.org/videostreaming/grayjay.git
synced 2025-08-02 22:30:40 +00:00
Merge branch 'hls-audio-fixes' into 'master'
HLS audio download See merge request videostreaming/grayjay!86
This commit is contained in:
commit
3b1b288478
4 changed files with 58 additions and 32 deletions
|
@ -322,7 +322,7 @@ class UISlideOverlays {
|
||||||
|
|
||||||
val masterPlaylist: HLS.MasterPlaylist
|
val masterPlaylist: HLS.MasterPlaylist
|
||||||
try {
|
try {
|
||||||
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl)
|
masterPlaylist = HLS.parseMasterPlaylist(masterPlaylistContent, sourceUrl, source is IHLSManifestAudioSource)
|
||||||
|
|
||||||
masterPlaylist.getAudioSources().forEach { it ->
|
masterPlaylist.getAudioSources().forEach { it ->
|
||||||
|
|
||||||
|
|
|
@ -630,10 +630,8 @@ class VideoDownload {
|
||||||
|
|
||||||
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
private suspend fun combineSegments(context: Context, segmentFiles: List<File>, targetFile: File) = withContext(Dispatchers.IO) {
|
||||||
suspendCancellableCoroutine { continuation ->
|
suspendCancellableCoroutine { continuation ->
|
||||||
val fileList = File(context.cacheDir, "fileList-${UUID.randomUUID()}.txt")
|
val cmd =
|
||||||
fileList.writeText(segmentFiles.joinToString("\n") { "file '${it.absolutePath}'" })
|
"-i \"concat:${segmentFiles.joinToString("|")}\" -c copy \"${targetFile.absolutePath}\""
|
||||||
|
|
||||||
val cmd = "-f concat -safe 0 -i \"${fileList.absolutePath}\" -c copy \"${targetFile.absolutePath}\""
|
|
||||||
|
|
||||||
val statisticsCallback = StatisticsCallback { _ ->
|
val statisticsCallback = StatisticsCallback { _ ->
|
||||||
//TODO: Show progress?
|
//TODO: Show progress?
|
||||||
|
@ -643,7 +641,6 @@ class VideoDownload {
|
||||||
val session = FFmpegKit.executeAsync(cmd,
|
val session = FFmpegKit.executeAsync(cmd,
|
||||||
{ session ->
|
{ session ->
|
||||||
if (ReturnCode.isSuccess(session.returnCode)) {
|
if (ReturnCode.isSuccess(session.returnCode)) {
|
||||||
fileList.delete()
|
|
||||||
continuation.resumeWith(Result.success(Unit))
|
continuation.resumeWith(Result.success(Unit))
|
||||||
} else {
|
} else {
|
||||||
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
val errorMessage = if (ReturnCode.isCancel(session.returnCode)) {
|
||||||
|
@ -651,7 +648,6 @@ class VideoDownload {
|
||||||
} else {
|
} else {
|
||||||
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
"Command failed with state '${session.state}' and return code ${session.returnCode}, stack trace ${session.failStackTrace}"
|
||||||
}
|
}
|
||||||
fileList.delete()
|
|
||||||
continuation.resumeWithException(RuntimeException(errorMessage))
|
continuation.resumeWithException(RuntimeException(errorMessage))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -1160,6 +1156,8 @@ class VideoDownload {
|
||||||
fun audioContainerToExtension(container: String): String {
|
fun audioContainerToExtension(container: String): String {
|
||||||
if (container.contains("audio/mp4"))
|
if (container.contains("audio/mp4"))
|
||||||
return "mp4a";
|
return "mp4a";
|
||||||
|
else if (container.contains("video/mp4"))
|
||||||
|
return "mp4";
|
||||||
else if (container.contains("audio/mpeg"))
|
else if (container.contains("audio/mpeg"))
|
||||||
return "mpga";
|
return "mpga";
|
||||||
else if (container.contains("audio/mp3"))
|
else if (container.contains("audio/mp3"))
|
||||||
|
|
|
@ -69,7 +69,7 @@ class VideoExport {
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (v != null) {
|
} else if (v != null) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.videoContainerToExtension(v.container);
|
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.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
Logger.i(TAG, "Copying video.");
|
Logger.i(TAG, "Copying video.");
|
||||||
|
@ -81,8 +81,8 @@ class VideoExport {
|
||||||
outputFile = f;
|
outputFile = f;
|
||||||
} else if (a != null) {
|
} else if (a != null) {
|
||||||
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
val outputFileName = videoLocal.name.sanitizeFileName(true) + "." + VideoDownload.audioContainerToExtension(a.container);
|
||||||
val f = downloadRoot.createFile(a.container, outputFileName)
|
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.");
|
?: throw Exception("Failed to create file in external directory.");
|
||||||
|
|
||||||
Logger.i(TAG, "Copying audio.");
|
Logger.i(TAG, "Copying audio.");
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,11 @@
|
||||||
package com.futo.platformplayer.parsers
|
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.HLSVariantAudioUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
|
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantSubtitleUrlSource
|
||||||
import com.futo.platformplayer.api.media.models.streams.sources.HLSVariantVideoUrlSource
|
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.api.media.models.streams.sources.IHLSManifestSource
|
||||||
import com.futo.platformplayer.toYesNo
|
import com.futo.platformplayer.toYesNo
|
||||||
import com.futo.platformplayer.yesNoToBoolean
|
import com.futo.platformplayer.yesNoToBoolean
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
|
import java.net.URLConnection
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
class HLS {
|
class HLS {
|
||||||
companion object {
|
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 baseUrl = URI(sourceUrl).resolve("./").toString()
|
||||||
|
|
||||||
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
val variantPlaylists = mutableListOf<VariantPlaylistReference>()
|
||||||
|
@ -21,27 +34,36 @@ class HLS {
|
||||||
val sessionDataList = mutableListOf<SessionData>()
|
val sessionDataList = mutableListOf<SessionData>()
|
||||||
var independentSegments = false
|
var independentSegments = false
|
||||||
|
|
||||||
masterPlaylistContent.lines().forEachIndexed { index, line ->
|
if (playlist is HlsMediaPlaylist) {
|
||||||
when {
|
independentSegments = playlist.hasIndependentSegments
|
||||||
line.startsWith("#EXT-X-STREAM-INF") -> {
|
if (isAudioSource == true) {
|
||||||
val nextLine = masterPlaylistContent.lines().getOrNull(index + 1)
|
mediaRenditions.add(MediaRendition("AUDIO", playlist.baseUri, "Single Playlist", null, null, null, null, null))
|
||||||
?: throw Exception("Expected URI following #EXT-X-STREAM-INF, found none")
|
} else {
|
||||||
val url = resolveUrl(baseUrl, nextLine)
|
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") -> {
|
line.startsWith("#EXT-X-MEDIA") -> {
|
||||||
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
mediaRenditions.add(parseMediaRendition(line, baseUrl))
|
||||||
}
|
}
|
||||||
|
|
||||||
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
line == "#EXT-X-INDEPENDENT-SEGMENTS" -> {
|
||||||
independentSegments = true
|
independentSegments = true
|
||||||
}
|
}
|
||||||
|
|
||||||
line.startsWith("#EXT-X-SESSION-DATA") -> {
|
line.startsWith("#EXT-X-SESSION-DATA") -> {
|
||||||
val sessionData = parseSessionData(line)
|
val sessionData = parseSessionData(line)
|
||||||
sessionDataList.add(sessionData)
|
sessionDataList.add(sessionData)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -61,7 +83,13 @@ class HLS {
|
||||||
val playlistType = lines.find { it.startsWith("#EXT-X-PLAYLIST-TYPE:") }?.substringAfter(":")
|
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 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>()
|
val segments = mutableListOf<Segment>()
|
||||||
|
if (initSegment != null) {
|
||||||
|
segments.add(MediaSegment(0.0, resolveUrl(sourceUrl, initSegment)))
|
||||||
|
}
|
||||||
var currentSegment: MediaSegment? = null
|
var currentSegment: MediaSegment? = null
|
||||||
lines.forEach { line ->
|
lines.forEach { line ->
|
||||||
when {
|
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
|
val masterPlaylist: MasterPlaylist
|
||||||
try {
|
try {
|
||||||
masterPlaylist = parseMasterPlaylist(content, url)
|
masterPlaylist = parseMasterPlaylist(content, url, isAudioSource)
|
||||||
return masterPlaylist.getAudioSources()
|
return masterPlaylist.getAudioSources()
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
if (content.lines().any { it.startsWith("#EXTINF:") }) {
|
||||||
|
@ -270,7 +298,7 @@ class HLS {
|
||||||
val name: String?,
|
val name: String?,
|
||||||
val isDefault: Boolean?,
|
val isDefault: Boolean?,
|
||||||
val isAutoSelect: Boolean?,
|
val isAutoSelect: Boolean?,
|
||||||
val isForced: Boolean?
|
val isForced: Boolean?,
|
||||||
) {
|
) {
|
||||||
fun toM3U8Line(): String = buildString {
|
fun toM3U8Line(): String = buildString {
|
||||||
append("#EXT-X-MEDIA:")
|
append("#EXT-X-MEDIA:")
|
||||||
|
@ -376,7 +404,7 @@ class HLS {
|
||||||
val programDateTime: ZonedDateTime?,
|
val programDateTime: ZonedDateTime?,
|
||||||
val playlistType: String?,
|
val playlistType: String?,
|
||||||
val streamInfo: StreamInfo?,
|
val streamInfo: StreamInfo?,
|
||||||
val segments: List<Segment>
|
val segments: List<Segment>,
|
||||||
) {
|
) {
|
||||||
fun buildM3U8(): String = buildString {
|
fun buildM3U8(): String = buildString {
|
||||||
append("#EXTM3U\n")
|
append("#EXTM3U\n")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue