WIP: initial repository setup for media.ccc streams

This uses the media.ccc.de URL as item-ID and the actual extractor to
fetch the streams.

Now we have a full top-to-bottom integration going, meaning we can
work on the stream selection based on actual data, not just made up
data.
This commit is contained in:
Profpatsch 2024-12-21 13:19:23 +01:00
parent b351692eea
commit 65220dfc4d
2 changed files with 111 additions and 2 deletions

View File

@ -51,6 +51,9 @@ import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import org.schabi.newpipe.App import org.schabi.newpipe.App
import org.schabi.newpipe.MainActivity import org.schabi.newpipe.MainActivity
import org.schabi.newpipe.extractor.ServiceList
import org.schabi.newpipe.extractor.services.media_ccc.extractors.MediaCCCStreamExtractor
import org.schabi.newpipe.extractor.services.media_ccc.extractors.data.MediaCCCRecording
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -61,7 +64,7 @@ object NewPlayerComponent {
fun provideNewPlayer(app: Application): NewPlayer { fun provideNewPlayer(app: Application): NewPlayer {
val player = NewPlayerImpl( val player = NewPlayerImpl(
app = app, app = app,
repository = PrefetchingRepository(CachingRepository(TestMediaRepository())), repository = PrefetchingRepository(CachingRepository(MediaCCCTestRepository())),
notificationIcon = IconCompat.createWithResource(app, net.newpipe.newplayer.R.drawable.new_player_tiny_icon), notificationIcon = IconCompat.createWithResource(app, net.newpipe.newplayer.R.drawable.new_player_tiny_icon),
playerActivityClass = MainActivity::class.java, playerActivityClass = MainActivity::class.java,
// rescueStreamFault = … // rescueStreamFault = …
@ -158,3 +161,109 @@ class TestMediaRepository() : MediaRepository {
override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) =
"" ""
} }
class MediaCCCTestRepository() : MediaRepository {
private val client = OkHttpClient()
private val service = ServiceList.MediaCCC
suspend fun fetchPage(item: String): MediaCCCStreamExtractor {
return withContext(Dispatchers.IO) {
// TODO: handle MediaCCCLiveStreamExtractor as well
val extractor = service.getStreamExtractor(item) as MediaCCCStreamExtractor
extractor.fetchPage()
extractor
}
}
override fun getRepoInfo() =
MediaRepository.RepoMetaInfo(canHandleTimestampedLinks = true, pullsDataFromNetwork = true)
@OptIn(UnstableApi::class)
override suspend fun getMetaInfo(item: String): MediaMetadata {
// parse as url
val extractor = fetchPage(item)
return MediaMetadata.Builder().apply {
setTitle(extractor.name)
setArtist(extractor.subChannelName)
setDurationMs(
1871L * 1000L
)
extractor.thumbnails.firstOrNull()?.url?.let {
setArtworkUri(Uri.parse(it))
}
}.build()
}
override suspend fun getStreams(item: String): List<Stream> {
val extractor = fetchPage(item)
return extractor.recordings.filterIsInstance<MediaCCCRecording.Video>()
.filter { it.recordingType == MediaCCCRecording.VideoType.MAIN }
.map { track ->
Stream(
item = item,
streamUri = Uri.parse(track.url),
streamTracks =
listOf(
VideoStreamTrack(
width = track.width,
height = track.height,
fileFormat = track.mimeType
),
) +
// one audio track per language
// (TODO: probably dont need to attach the audio track here?)
track.languages.map { language ->
AudioStreamTrack(
// TODO: should we pass the Locale instead??
language = language.language,
fileFormat = track.mimeType,
// TODO: thats something ExoPlayer should determine for us,
// we dont know that from the metadata
bitrate = 44100,
)
}
)
}
}
override suspend fun getSubtitles(item: String) =
emptyList<Subtitle>()
override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long): Bitmap? {
val extractor = fetchPage(item)
val templateUrl = extractor.thumbnails.firstOrNull()?.url ?: return null
val thumbnailId = (timestampInMs / (10 * 1000)) + 1
if (getPreviewThumbnailsInfo(item).count < thumbnailId) {
return null
}
val thumbUrl = String.format(templateUrl, thumbnailId)
val bitmap = withContext(Dispatchers.IO) {
val request = Request.Builder().url(thumbUrl).build()
val response = client.newCall(request).execute()
try {
val responseBody = response.body
val bitmap = BitmapFactory.decodeStream(responseBody?.byteStream())
return@withContext bitmap
} catch (e: Exception) {
return@withContext null
}
}
return bitmap
}
override suspend fun getPreviewThumbnailsInfo(item: String) =
MediaRepository.PreviewThumbnailsInfo(1, 0)
override suspend fun getChapters(item: String) =
listOf<Chapter>()
override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) =
""
}

View File

@ -1182,7 +1182,7 @@ public final class VideoDetailFragment
final PlayQueue queue = setupPlayQueueForIntent(false); final PlayQueue queue = setupPlayQueueForIntent(false);
tryAddVideoPlayerView(); tryAddVideoPlayerView();
newPlayer.playStream("bgp", PlayMode.EMBEDDED_VIDEO); newPlayer.playStream("https://media.ccc.de/v/34c3-9072-bgp_and_the_rule_of_custom", PlayMode.EMBEDDED_VIDEO);
newPlayer.setPlayWhenReady(true); newPlayer.setPlayWhenReady(true);
newPlayer.prepare(); newPlayer.prepare();