LibreTube/app/src/main/java/com/github/libretube/helpers/DashHelper.kt

195 lines
7.3 KiB
Kotlin
Raw Normal View History

package com.github.libretube.helpers
2022-11-16 19:47:27 +05:30
import com.github.libretube.api.obj.PipedStream
import com.github.libretube.api.obj.Streams
import java.io.StringWriter
import javax.xml.parsers.DocumentBuilderFactory
import javax.xml.transform.TransformerFactory
import javax.xml.transform.dom.DOMSource
import javax.xml.transform.stream.StreamResult
import org.w3c.dom.Document
import org.w3c.dom.Element
2022-11-16 19:47:27 +05:30
// Based off of https://github.com/TeamPiped/Piped/blob/master/src/utils/DashUtils.js
2022-11-16 21:26:23 +05:30
object DashHelper {
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
private val builderFactory: DocumentBuilderFactory = DocumentBuilderFactory.newInstance()
private val transformerFactory: TransformerFactory = TransformerFactory.newInstance()
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
private data class AdapSetInfo(
val mimeType: String,
val audioTrackId: String? = null,
val formats: MutableList<PipedStream> = mutableListOf()
2022-11-16 21:26:23 +05:30
)
2022-11-16 19:47:27 +05:30
fun createManifest(streams: Streams, supportsHdr: Boolean, audioOnly: Boolean = false): String {
val builder = builderFactory.newDocumentBuilder()
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val doc = builder.newDocument()
val mpd = doc.createElement("MPD")
mpd.setAttribute("xmlns", "urn:mpeg:dash:schema:mpd:2011")
mpd.setAttribute("profiles", "urn:mpeg:dash:profile:full:2011")
mpd.setAttribute("minBufferTime", "PT1.5S")
mpd.setAttribute("type", "static")
mpd.setAttribute("mediaPresentationDuration", "PT${streams.duration}S")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val period = doc.createElement("Period")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val adapSetInfos = ArrayList<AdapSetInfo>()
2022-11-16 19:47:27 +05:30
if (!audioOnly) {
val enabledVideoCodecs = PlayerHelper.enabledVideoCodecs
for (
stream in streams.videoStreams
// used to avoid including LBRY HLS inside the streams in the manifest
.filter { !it.format.orEmpty().contains("HLS") }
// filter the codecs according to the user's preferences
.filter {
enabledVideoCodecs == "all" || it.codec.orEmpty().lowercase().startsWith(
enabledVideoCodecs
)
}
.filter { supportsHdr || !it.quality.orEmpty().uppercase().contains("HDR") }
) {
// ignore dual format streams
if (!stream.videoOnly!!) {
continue
}
2022-11-16 19:47:27 +05:30
// ignore streams which might be OTF
if (stream.indexEnd!! <= 0) {
continue
}
val adapSetInfo = adapSetInfos.find { it.mimeType == stream.mimeType }
if (adapSetInfo != null) {
adapSetInfo.formats.add(stream)
continue
}
adapSetInfos.add(
AdapSetInfo(
stream.mimeType!!,
null,
mutableListOf(stream)
)
)
2022-11-16 21:26:23 +05:30
}
}
2023-02-08 14:11:59 +05:30
for (stream in streams.audioStreams) {
2022-11-16 21:26:23 +05:30
val adapSetInfo =
2022-12-19 21:28:34 +05:30
adapSetInfos.find {
it.mimeType == stream.mimeType && it.audioTrackId == stream.audioTrackId
}
2022-11-16 21:26:23 +05:30
if (adapSetInfo != null) {
adapSetInfo.formats.add(stream)
continue
2022-11-16 19:47:27 +05:30
}
2022-11-16 21:26:23 +05:30
adapSetInfos.add(
AdapSetInfo(
stream.mimeType!!,
stream.audioTrackId,
mutableListOf(stream)
)
2022-11-16 21:26:23 +05:30
)
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
for (adapSet in adapSetInfos) {
val adapSetElement = doc.createElement("AdaptationSet")
adapSetElement.setAttribute("mimeType", adapSet.mimeType)
adapSetElement.setAttribute("startWithSAP", "1")
adapSetElement.setAttribute("subsegmentAlignment", "true")
if (adapSet.audioTrackId != null) {
adapSetElement.setAttribute("lang", adapSet.audioTrackId.substring(0, 2))
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val isVideo = adapSet.mimeType.contains("video")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
if (isVideo) {
adapSetElement.setAttribute("scanType", "progressive")
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
for (stream in adapSet.formats) {
val rep = let {
if (isVideo) {
createVideoRepresentation(doc, stream)
} else {
createAudioRepresentation(doc, stream)
2022-11-16 19:47:27 +05:30
}
}
2022-11-16 21:26:23 +05:30
adapSetElement.appendChild(rep)
2022-11-16 19:47:27 +05:30
}
2022-11-16 21:26:23 +05:30
period.appendChild(adapSetElement)
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
mpd.appendChild(period)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
doc.appendChild(mpd)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val domSource = DOMSource(doc)
val writer = StringWriter()
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val transformer = transformerFactory.newTransformer()
transformer.transform(domSource, StreamResult(writer))
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
return writer.toString()
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
private fun createAudioRepresentation(doc: Document, stream: PipedStream): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("bandwidth", stream.bitrate.toString())
representation.setAttribute("codecs", stream.codec!!)
representation.setAttribute("mimeType", stream.mimeType!!)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val audioChannelConfiguration = doc.createElement("AudioChannelConfiguration")
audioChannelConfiguration.setAttribute(
"schemeIdUri",
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011"
2022-11-16 21:26:23 +05:30
)
audioChannelConfiguration.setAttribute("value", "2")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val baseUrl = doc.createElement("BaseURL")
2023-03-11 20:41:43 +05:30
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapIfEnabled(stream.url!!)))
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val initialization = doc.createElement("Initialization")
initialization.setAttribute("range", "${stream.initStart}-${stream.initEnd}")
segmentBase.appendChild(initialization)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
representation.appendChild(audioChannelConfiguration)
representation.appendChild(baseUrl)
representation.appendChild(segmentBase)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
return representation
}
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
private fun createVideoRepresentation(doc: Document, stream: PipedStream): Element {
val representation = doc.createElement("Representation")
representation.setAttribute("codecs", stream.codec!!)
representation.setAttribute("bandwidth", stream.bitrate.toString())
representation.setAttribute("width", stream.width.toString())
representation.setAttribute("height", stream.height.toString())
representation.setAttribute("maxPlayoutRate", "1")
representation.setAttribute("frameRate", stream.fps.toString())
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val baseUrl = doc.createElement("BaseURL")
2023-03-11 20:41:43 +05:30
baseUrl.appendChild(doc.createTextNode(ProxyHelper.unwrapIfEnabled(stream.url!!)))
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val segmentBase = doc.createElement("SegmentBase")
segmentBase.setAttribute("indexRange", "${stream.indexStart}-${stream.indexEnd}")
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
val initialization = doc.createElement("Initialization")
initialization.setAttribute("range", "${stream.initStart}-${stream.initEnd}")
segmentBase.appendChild(initialization)
2022-11-16 19:47:27 +05:30
2022-11-16 21:26:23 +05:30
representation.appendChild(baseUrl)
representation.appendChild(segmentBase)
return representation
2022-11-16 19:47:27 +05:30
}
}