Skip to content

Commit

Permalink
av1, h265 support
Browse files Browse the repository at this point in the history
  • Loading branch information
crackededed committed Jan 27, 2024
1 parent 917e17f commit 2db3b0b
Show file tree
Hide file tree
Showing 11 changed files with 90 additions and 27 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ android {
minSdk = 21
targetSdk = 34
versionCode = 121
versionName = "2.27.4"
versionName = "2.28.0"
resourceConfigurations += listOf("ar", "de", "en", "es", "fr", "in", "ja", "pt-rBR", "ru", "tr", "zh-rTW")
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import org.json.JSONObject
import retrofit2.Response
import java.net.InetSocketAddress
import java.net.Proxy
import java.net.URLEncoder
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
Expand Down Expand Up @@ -72,7 +73,7 @@ class PlayerRepository @Inject constructor(
}.build()).execute().use { it.body.string() }
}

suspend fun loadStreamPlaylistUrl(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, proxyPlaybackAccessToken: Boolean, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?): String = withContext(Dispatchers.IO) {
suspend fun loadStreamPlaylistUrl(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, supportedCodecs: String?, proxyPlaybackAccessToken: Boolean, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?): String = withContext(Dispatchers.IO) {
val accessTokenHeaders = getPlaybackAccessTokenHeaders(gqlHeaders, randomDeviceId, xDeviceId)
val accessToken = if (proxyPlaybackAccessToken && !proxyHost.isNullOrBlank() && proxyPort != null) {
val json = JsonObject().apply {
Expand Down Expand Up @@ -119,8 +120,10 @@ class PlayerRepository @Inject constructor(
"allow_audio_only", "true",
"fast_bread", "true", //low latency
"p", Random.nextInt(9999999).toString(),
"sig", accessToken?.signature ?: "",
"token", accessToken?.token ?: ""
"platform", if (supportedCodecs?.contains("av1", true) == true) "web" else null,
"sig", accessToken?.signature,
"supported_codecs", supportedCodecs,
"token", accessToken?.token
).toString()
if (proxyMultivariantPlaylist) {
val response = getResponse(
Expand All @@ -134,15 +137,17 @@ class PlayerRepository @Inject constructor(
} else url
}

suspend fun loadVideoPlaylistUrl(gqlHeaders: Map<String, String>, videoId: String?, playerType: String?): Uri = withContext(Dispatchers.IO) {
suspend fun loadVideoPlaylistUrl(gqlHeaders: Map<String, String>, videoId: String?, playerType: String?, supportedCodecs: String?): Uri = withContext(Dispatchers.IO) {
val accessToken = loadVideoPlaybackAccessToken(gqlHeaders, videoId, playerType)
buildUrl(
"https://usher.ttvnw.net/vod/$videoId.m3u8?",
"allow_source", "true",
"allow_audio_only", "true",
"p", Random.nextInt(9999999).toString(),
"sig", accessToken?.signature ?: "",
"token", accessToken?.token ?: "",
"platform", if (supportedCodecs?.contains("av1", true) == true) "web" else null,
"sig", accessToken?.signature,
"supported_codecs", supportedCodecs,
"token", accessToken?.token,
)
}

Expand Down Expand Up @@ -182,16 +187,19 @@ class PlayerRepository @Inject constructor(
}
}

private fun buildUrl(url: String, vararg queryParams: String): Uri {
private fun buildUrl(url: String, vararg queryParams: String?): Uri {
val stringBuilder = StringBuilder(url)
stringBuilder.append(queryParams[0])
.append("=")
.append(queryParams[1])
.append(URLEncoder.encode(queryParams[1], Charsets.UTF_8.name()))
for (i in 2 until queryParams.size step 2) {
stringBuilder.append("&")
.append(queryParams[i])
.append("=")
.append(queryParams[i + 1])
val value = queryParams[i + 1]
if (!value.isNullOrBlank()) {
stringBuilder.append("&")
.append(queryParams[i])
.append("=")
.append(URLEncoder.encode(value, Charsets.UTF_8.name()))
}
}
return stringBuilder.toString().toUri()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.fragment.app.FragmentTransaction
import androidx.media3.common.MimeTypes
import androidx.media3.exoplayer.mediacodec.MediaCodecSelector
import androidx.navigation.NavController
import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.ui.NavigationUI
Expand Down Expand Up @@ -159,6 +161,19 @@ class MainActivity : AppCompatActivity(), SlidingLayout.Listener {
putBoolean(C.FIRST_LAUNCH6, false)
}
}
if (prefs.getBoolean(C.FIRST_LAUNCH7, true)) {
prefs.edit {
when {
MediaCodecSelector.DEFAULT.getDecoderInfos(MimeTypes.VIDEO_H265, false, false).none { it.hardwareAccelerated } -> {
putString(C.TOKEN_SUPPORTED_CODECS, "h264")
}
MediaCodecSelector.DEFAULT.getDecoderInfos(MimeTypes.VIDEO_AV1, false, false).none { it.hardwareAccelerated } -> {
putString(C.TOKEN_SUPPORTED_CODECS, "h265,h264")
}
}
putBoolean(C.FIRST_LAUNCH7, false)
}
}
viewModel.integrity.observe(this) {
if (prefs.getBoolean(C.ENABLE_INTEGRITY, false) && prefs.getBoolean(C.USE_WEBVIEW_INTEGRITY, true)) {
IntegrityDialog.show(supportFragmentManager)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,49 @@ class PlaybackService : MediaSessionService() {
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
val manifest = mediaSession?.player?.currentManifest as? HlsManifest
if (urls.isEmpty() && manifest is HlsManifest) {
manifest.multivariantPlaylist.let {
val tags = it.tags
manifest.multivariantPlaylist.let { playlist ->
val tags = playlist.tags
val qualityNames = mutableListOf<String>()
val codecs = mutableListOf<String>()
val map = mutableMapOf<String, String>()
val appContext = XtraApp.INSTANCE.applicationContext
val audioOnly = ContextCompat.getString(appContext, R.string.audio_only)
val pattern = Pattern.compile("NAME=\"(.+?)\"")
val qualityPattern = Pattern.compile("NAME=\"(.+?)\"")
val codecPattern = Pattern.compile("CODECS=\"(.+?)\\.")
var trackIndex = 0
tags.forEach { tag ->
if (tag.startsWith("#EXT-X-MEDIA")) {
val matcher = pattern.matcher(tag)
val matcher = qualityPattern.matcher(tag)
if (matcher.find()) {
val quality = matcher.group(1)!!
val url = it.variants[trackIndex++].url.toString()
map[if (!quality.startsWith("audio", true)) quality else audioOnly] = url
qualityNames.add(quality)
}
}
if (tag.startsWith("#EXT-X-STREAM-INF")) {
val matcher = codecPattern.matcher(tag)
if (matcher.find()) {
val codec = matcher.group(1)!!
codecs.add(when(codec) {
"av01" -> "AV1"
"hvc1" -> "H.265"
"avc1" -> "H.264"
else -> codec
})
}
}
}
if (codecs.all { it == "H.264" || it == "mp4a" }) {
codecs.clear()
}
qualityNames.forEachIndexed { index, quality ->
val url = playlist.variants[trackIndex++].url.toString()
map[if (!quality.startsWith("audio", true)) {
codecs.getOrNull(index)?.let { codec ->
"$quality $codec"
} ?: quality
} else {
audioOnly
}] = url
}
urls = map.apply {
if (containsKey(audioOnly)) {
Expand Down Expand Up @@ -752,8 +779,8 @@ class PlaybackService : MediaSessionService() {
}

private fun setQualityIndex() {
val defaultQuality = prefs().getString(C.PLAYER_DEFAULTQUALITY, "saved")
val savedQuality = prefs().getString(C.PLAYER_QUALITY, "720p60")
val defaultQuality = prefs().getString(C.PLAYER_DEFAULTQUALITY, "saved")?.substringBefore(" ")
val savedQuality = prefs().getString(C.PLAYER_QUALITY, "720p60")?.substringBefore(" ")
val index = when (defaultQuality) {
"Source" -> {
if (usingAutoQuality) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,7 @@ class StreamPlayerFragment : BasePlayerFragment() {
randomDeviceId = prefs.getBoolean(C.TOKEN_RANDOM_DEVICEID, true),
xDeviceId = prefs.getString(C.TOKEN_XDEVICEID, "twitch-web-wall-mason"),
playerType = prefs.getString(C.TOKEN_PLAYERTYPE, "site"),
supportedCodecs = prefs.getString(C.TOKEN_SUPPORTED_CODECS, "av1,h265,h264"),
proxyPlaybackAccessToken = prefs.getBoolean(C.PROXY_PLAYBACK_ACCESS_TOKEN, false),
proxyMultivariantPlaylist = proxyMultivariantPlaylist,
proxyHost = proxyHost,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,10 +70,10 @@ class StreamPlayerViewModel @Inject constructor(
}
}

fun load(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, proxyPlaybackAccessToken: Boolean, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?) {
fun load(gqlHeaders: Map<String, String>, channelLogin: String, randomDeviceId: Boolean?, xDeviceId: String?, playerType: String?, supportedCodecs: String?, proxyPlaybackAccessToken: Boolean, proxyMultivariantPlaylist: Boolean, proxyHost: String?, proxyPort: Int?, proxyUser: String?, proxyPassword: String?) {
viewModelScope.launch {
try {
playerRepository.loadStreamPlaylistUrl(gqlHeaders, channelLogin, randomDeviceId, xDeviceId, playerType, proxyPlaybackAccessToken, proxyMultivariantPlaylist, proxyHost, proxyPort, proxyUser, proxyPassword)
playerRepository.loadStreamPlaylistUrl(gqlHeaders, channelLogin, randomDeviceId, xDeviceId, playerType, supportedCodecs, proxyPlaybackAccessToken, proxyMultivariantPlaylist, proxyHost, proxyPort, proxyUser, proxyPassword)
} catch (e: Exception) {
if (e.message == "failed integrity check") {
_integrity.postValue(true)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,8 @@ class VideoPlayerFragment : BasePlayerFragment(), HasDownloadDialog, ChatReplayP
viewModel.load(
gqlHeaders = TwitchApiHelper.getGQLHeaders(requireContext(), prefs.getBoolean(C.TOKEN_INCLUDE_TOKEN_VIDEO, true)),
videoId = video.id,
playerType = prefs.getString(C.TOKEN_PLAYERTYPE_VIDEO, "channel_home_live")
playerType = prefs.getString(C.TOKEN_PLAYERTYPE_VIDEO, "channel_home_live"),
supportedCodecs = prefs.getString(C.TOKEN_SUPPORTED_CODECS, "av1,h265,h264")
)
viewModel.result.observe(viewLifecycleOwner) { url ->
player?.sendCustomCommand(SessionCommand(PlaybackService.START_VIDEO, bundleOf(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ class VideoPlayerViewModel @Inject constructor(
val gamesList = MutableLiveData<List<Game>>()
var shouldRetry = true

fun load(gqlHeaders: Map<String, String>, videoId: String?, playerType: String?) {
fun load(gqlHeaders: Map<String, String>, videoId: String?, playerType: String?, supportedCodecs: String?) {
viewModelScope.launch {
try {
playerRepository.loadVideoPlaylistUrl(gqlHeaders, videoId, playerType)
playerRepository.loadVideoPlaylistUrl(gqlHeaders, videoId, playerType, supportedCodecs)
} catch (e: Exception) {
if (e.message == "failed integrity check") {
_integrity.postValue(true)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/com/github/andreyasadchy/xtra/util/C.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ object C {
const val FIRST_LAUNCH4 = "first_launch4"
const val FIRST_LAUNCH5 = "first_launch5"
const val FIRST_LAUNCH6 = "first_launch6"
const val FIRST_LAUNCH7 = "first_launch7"
const val LANDSCAPE_CHAT_WIDTH = "landscape_chat_width"
const val KEY_CHAT_OPENED = "key_chat_opened"
const val KEY_CHAT_BAR_VISIBLE = "key_chat_bar_visible"
Expand Down Expand Up @@ -176,6 +177,7 @@ object C {
const val TOKEN_PLAYERTYPE_VIDEO = "token_playertype_video"
const val TOKEN_INCLUDE_TOKEN_STREAM = "token_include_token_stream"
const val TOKEN_INCLUDE_TOKEN_VIDEO = "token_include_token_video"
const val TOKEN_SUPPORTED_CODECS = "token_supported_codecs"
const val TOKEN_SKIP_VIDEO_ACCESS_TOKEN = "token_skip_video_access_token"
const val TOKEN_SKIP_CLIP_ACCESS_TOKEN = "token_skip_clip_access_token"
const val HELIX = "Helix"
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/res/xml/token_preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@
app:iconSpaceReserved="false"
app:singleLineTitle="false" />

<EditTextPreference
android:defaultValue="av1,h265,h264"
android:key="token_supported_codecs"
android:title="supported_codecs"
app:allowDividerAbove="true"
app:iconSpaceReserved="false"
app:singleLineTitle="false"
app:useSimpleSummaryProvider="true" />

<ListPreference
android:defaultValue="2"
android:entries="@array/skipVideoTokenEntries"
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ preference = "1.2.1"
retrofit = "2.9.0"
room = "2.6.1"
swiperefreshlayout = "1.1.0"
webkit = "1.9.0"
webkit = "1.10.0"
work = "2.9.0"

[libraries]
Expand Down

0 comments on commit 2db3b0b

Please sign in to comment.