make initial logic for NewPlayer and MediaReop

This commit is contained in:
Christian Schabesberger 2024-08-24 19:09:35 +02:00
parent ea099253a1
commit 47ad16c03d
12 changed files with 159 additions and 61 deletions

View File

@ -31,17 +31,18 @@ interface MediaRepository {
suspend fun getTitle(item: String) : String suspend fun getTitle(item: String) : String
suspend fun getChannelName(item: String): String suspend fun getChannelName(item: String): String
suspend fun getThumbnail(item: String): Thumbnail suspend fun getThumbnail(item: String): Thumbnail
suspend fun getAvailableStreamVariants(item: String): List<String>
suspend fun getAvailableSubtitleVariants(item: String): List<String>
suspend fun getAvailableStreamVariants(item: String): List<String>
suspend fun getStream(item: String, streamSelector: String) : Uri suspend fun getStream(item: String, streamSelector: String) : Uri
suspend fun getSubtitle(item: String, )
suspend fun getAvailableSubtitleVariants(item: String): List<String>
suspend fun getSubtitle(item: String, variant: String): Uri
suspend fun getPreviewThumbnails(item: String) : HashMap<Long, Thumbnail>? suspend fun getPreviewThumbnails(item: String) : HashMap<Long, Thumbnail>?
suspend fun getChapters(item: String): List<Chapter> suspend fun getChapters(item: String): List<Chapter>
suspend fun getChapterThumbnail(item: String, chapter: Long) : Thumbnail suspend fun getChapterThumbnail(item: String, chapter: Long) : Thumbnail?
suspend fun getTimestampLink(item: String, timestampInSeconds: Long) suspend fun getTimestampLink(item: String, timestampInSeconds: Long): String
suspend fun tryAndRescueError(item: String?, exception: PlaybackException) : Uri? suspend fun tryAndRescueError(item: String?, exception: PlaybackException) : Uri?
} }

View File

@ -128,7 +128,7 @@ class NewPlayerImpl(
override var fastSeekAmountSec: Int = 10 override var fastSeekAmountSec: Int = 10
override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO override var playBackMode: PlayMode = PlayMode.EMBEDDED_VIDEO
private var playerScope = CoroutineScope(Dispatchers.Default + Job()) private var playerScope = CoroutineScope(Dispatchers.Main + Job())
override var playMode = MutableStateFlow<PlayMode?>(null) override var playMode = MutableStateFlow<PlayMode?>(null)
@ -192,7 +192,7 @@ class NewPlayerImpl(
override fun playStream(item: String, streamVariant: String, playMode: PlayMode) { override fun playStream(item: String, streamVariant: String, playMode: PlayMode) {
launchJobAndCollectError { launchJobAndCollectError {
val stream = toMediaItem(item) val stream = toMediaItem(item, streamVariant)
internalPlayStream(stream, playMode) internalPlayStream(stream, playMode)
} }
} }
@ -202,6 +202,8 @@ class NewPlayerImpl(
internalPlayer.prepare() internalPlayer.prepare()
} }
this.playMode.update { playMode } this.playMode.update { playMode }
this.internalPlayer.setMediaItem(mediaItem)
this.internalPlayer.play()
} }
private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem { private suspend fun toMediaItem(item: String, streamVariant: String): MediaItem {

View File

@ -98,7 +98,8 @@ enum class UIModeState {
companion object { companion object {
fun fromPlayMode(playMode: PlayMode) = fun fromPlayMode(playMode: PlayMode?) =
if (playMode != null)
when (playMode) { when (playMode) {
PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO PlayMode.EMBEDDED_VIDEO -> EMBEDDED_VIDEO
PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO PlayMode.FULLSCREEN_VIDEO -> FULLSCREEN_VIDEO
@ -106,5 +107,7 @@ enum class UIModeState {
PlayMode.BACKGROUND -> TODO() PlayMode.BACKGROUND -> TODO()
PlayMode.AUDIO_FOREGROUND -> TODO() PlayMode.AUDIO_FOREGROUND -> TODO()
} }
else PLACEHOLDER
} }
} }

View File

@ -28,6 +28,7 @@ import android.util.Log
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat.getSystemService import androidx.core.content.ContextCompat.getSystemService
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.media3.common.Player import androidx.media3.common.Player
@ -77,7 +78,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
override var newPlayer: NewPlayer? = null override var newPlayer: NewPlayer? = null
set(value) { set(value) {
field = value field = value
installExoPlayer() installNewPlayer()
} }
override val uiState = mutableUiState.asStateFlow() override val uiState = mutableUiState.asStateFlow()
@ -118,7 +119,7 @@ class VideoPlayerViewModelImpl @Inject constructor(
} }
} }
private fun installExoPlayer() { private fun installNewPlayer() {
internalPlayer?.let { player -> internalPlayer?.let { player ->
Log.d(TAG, "Install player: ${player.videoSize.width}") Log.d(TAG, "Install player: ${player.videoSize.width}")
@ -137,17 +138,29 @@ class VideoPlayerViewModelImpl @Inject constructor(
updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize)) updateContentRatio(VideoSize.fromMedia3VideoSize(videoSize))
} }
// TODO: This is not correctly applicable for loading indicator
override fun onIsLoadingChanged(isLoading: Boolean) { override fun onIsLoadingChanged(isLoading: Boolean) {
super.onIsLoadingChanged(isLoading) super.onIsLoadingChanged(isLoading)
mutableUiState.update { mutableUiState.update {
it.copy(isLoading = isLoading) it.copy(isLoading = isLoading)
} }
Log.i(
TAG, if (isLoading) "Player started loading" else "Player finished loading"
)
} }
}) })
} }
newPlayer?.let{ newPlayer ->
viewModelScope.launch {
while(true) {
newPlayer.playMode.collect { mode ->
println("blub: $mode")
mutableUiState.update {
it.copy(uiMode = UIModeState.fromPlayMode(mode))
}
}
}
}
}
} }
fun updateContentRatio(videoSize: VideoSize) { fun updateContentRatio(videoSize: VideoSize) {

View File

@ -31,6 +31,10 @@ android {
namespace = "net.newpipe.newplayer.testapp" namespace = "net.newpipe.newplayer.testapp"
compileSdk = 34 compileSdk = 34
viewBinding {
enable = true
}
buildFeatures { buildFeatures {
compose = true compose = true
} }

View File

@ -38,6 +38,7 @@ import net.newpipe.newplayer.PlayMode
import net.newpipe.newplayer.VideoPlayerView import net.newpipe.newplayer.VideoPlayerView
import net.newpipe.newplayer.model.VideoPlayerViewModel import net.newpipe.newplayer.model.VideoPlayerViewModel
import net.newpipe.newplayer.model.VideoPlayerViewModelImpl import net.newpipe.newplayer.model.VideoPlayerViewModelImpl
import net.newpipe.newplayer.testapp.databinding.ActivityMainBinding
import net.newpipe.newplayer.ui.ContentScale import net.newpipe.newplayer.ui.ContentScale
import javax.inject.Inject import javax.inject.Inject
@ -51,20 +52,17 @@ class MainActivity : AppCompatActivity() {
var activityBrainSlug: ActivityBrainSlug? = null var activityBrainSlug: ActivityBrainSlug? = null
lateinit var binding: ActivityMainBinding
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
enableEdgeToEdge() enableEdgeToEdge()
setContentView(R.layout.activity_main) binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
val embeddedPlayer = findViewById<VideoPlayerView>(R.id.new_player_video_view) binding.startStreamButton.setOnClickListener {
val startStreamButton = findViewById<Button>(R.id.start_stream_button)
val buttonsLayout = findViewById<View>(R.id.buttons_layout)
val embeddedPlayerLayout = findViewById<View>(R.id.player_column)
val fullscreenPlayer = findViewById<VideoPlayerView>(R.id.fullscreen_player)
startStreamButton.setOnClickListener {
newPlayer.playWhenReady = true newPlayer.playWhenReady = true
newPlayer.playStream(getString(R.string.ccc_6502_video), PlayMode.EMBEDDED_VIDEO) newPlayer.playStream("6502", PlayMode.EMBEDDED_VIDEO)
} }
videoPlayerViewModel.newPlayer = newPlayer videoPlayerViewModel.newPlayer = newPlayer
@ -72,10 +70,10 @@ class MainActivity : AppCompatActivity() {
activityBrainSlug = ActivityBrainSlug(videoPlayerViewModel) activityBrainSlug = ActivityBrainSlug(videoPlayerViewModel)
activityBrainSlug?.let { activityBrainSlug?.let {
it.embeddedPlayerView = embeddedPlayer it.embeddedPlayerView = binding.embeddedPlayer
it.addViewToHideOnFullscreen(buttonsLayout) it.addViewToHideOnFullscreen(binding.buttonsLayout as View)
it.addViewToHideOnFullscreen(embeddedPlayerLayout) it.addViewToHideOnFullscreen(binding.embeddedPlayerLayout as View)
it.fullscreenPlayerView = fullscreenPlayer it.fullscreenPlayerView = binding.fullscreenPlayer
it.rootView = findViewById(R.id.main) it.rootView = findViewById(R.id.main)
} }
} }

View File

@ -21,7 +21,22 @@
package net.newpipe.newplayer.testapp package net.newpipe.newplayer.testapp
import android.app.Application import android.app.Application
import android.util.Log
import dagger.hilt.android.HiltAndroidApp import dagger.hilt.android.HiltAndroidApp
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer
import javax.inject.Inject
private const val TAG = "NewPlayerApp"
@HiltAndroidApp @HiltAndroidApp
class NewPlayerApp : Application() class NewPlayerApp : Application() {
val appScope = CoroutineScope(Dispatchers.Default + Job())
}

View File

@ -21,19 +21,33 @@
package net.newpipe.newplayer.testapp package net.newpipe.newplayer.testapp
import android.app.Application import android.app.Application
import android.util.Log
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.launch
import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayer
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@InstallIn(SingletonComponent::class) @InstallIn(SingletonComponent::class)
object NewPlayerComponent { object NewPlayerComponent {
@Provides @Provides
@Singleton @Singleton
fun provideNewPlayer(app: Application) : NewPlayer { fun provideNewPlayer(app: Application) : NewPlayer {
return NewPlayer.Builder(app, TestMediaRepository(app)).build() val player = NewPlayer.Builder(app, TestMediaRepository(app)).build()
if(app is NewPlayerApp) {
app.appScope.launch {
while(true) {
player.errorFlow.collect { e ->
Log.e("NewPlayerException", e.stackTraceToString())
}
}
}
}
return player
} }
} }

View File

@ -6,6 +6,7 @@ import android.graphics.BitmapFactory
import android.media.Image import android.media.Image
import android.net.Uri import android.net.Uri
import androidx.media3.common.MediaItem import androidx.media3.common.MediaItem
import androidx.media3.common.PlaybackException
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import net.newpipe.newplayer.Chapter import net.newpipe.newplayer.Chapter
@ -65,9 +66,13 @@ class TestMediaRepository(val context: Context) : MediaRepository {
else -> throw Exception("Unknown stream: $item") else -> throw Exception("Unknown stream: $item")
} }
override suspend fun getAvailableSubtitleVariants(item: String): List<String> {
TODO("Not yet implemented")
}
override suspend fun getStream(item: String, streamSelector: String) = override suspend fun getStream(item: String, streamSelector: String) =
MediaItem.fromUri( Uri.parse(
when (item) { when (item) {
"6502" -> context.getString(R.string.ccc_6502_video) "6502" -> context.getString(R.string.ccc_6502_video)
"portrait" -> context.getString(R.string.portrait_video_example) "portrait" -> context.getString(R.string.portrait_video_example)
@ -81,9 +86,13 @@ class TestMediaRepository(val context: Context) : MediaRepository {
} }
) )
override suspend fun getLinkWithStreamOffset(item: String): String { override suspend fun getSubtitle(item: String, variant: String) =
TODO("Not yet implemented") Uri.parse(
when (item) {
"imu" -> context.getString(R.string.ccc_imu_subtitles)
else -> ""
} }
)
override suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>? { override suspend fun getPreviewThumbnails(item: String): HashMap<Long, Thumbnail>? {
val templateUrl = when (item) { val templateUrl = when (item) {
@ -93,16 +102,17 @@ class TestMediaRepository(val context: Context) : MediaRepository {
else -> throw Exception("Unknown stream: $item") else -> throw Exception("Unknown stream: $item")
} }
if(templateUrl != null) { if (templateUrl != null) {
val thumbCount = when(item) { val thumbCount = when (item) {
"6502" -> 312 "6502" -> 312
"imu" -> 361 "imu" -> 361
else -> throw Exception("Unknown stream: $item") } else -> throw Exception("Unknown stream: $item")
}
var thumbMap = HashMap<Long, Thumbnail>() var thumbMap = HashMap<Long, Thumbnail>()
for (i in 1..thumbCount) { for (i in 1..thumbCount) {
val timeStamp= (i-1) * 10 * 1000 val timeStamp = (i - 1) * 10 * 1000
thumbMap.put(timeStamp.toLong(), OnlineThumbnail(String.format(templateUrl, i))) thumbMap.put(timeStamp.toLong(), OnlineThumbnail(String.format(templateUrl, i)))
} }
@ -112,11 +122,43 @@ class TestMediaRepository(val context: Context) : MediaRepository {
} }
} }
override suspend fun getChapters(item: String): List<Chapter> { override suspend fun getChapters(item: String) =
TODO("Not yet implemented") when (item) {
"6502" -> context.resources.getIntArray(R.array.ccc_6502_chapters)
"imu" -> TODO()
else -> intArrayOf()
}.map {
Chapter(it.toLong(), "Dummy Chapter at timestamp $it")
} }
override suspend fun getChapterThumbnail(item: String, chapter: Long): Thumbnail { override suspend fun getChapterThumbnail(item: String, chapter: Long) =
when (item) {
"6502" -> OnlineThumbnail(
String.format(
context.getString(R.string.ccc_6502_preview_thumbnails),
chapter / (10 * 1000)
)
)
"imu" -> OnlineThumbnail(
String.format(
context.getString(R.string.ccc_imu_preview_thumbnails),
chapter / (10 * 1000)
)
)
else -> null
}
override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) =
when (item) {
"6502" -> "${context.getString(R.string.ccc_6502_link)}#t=$timestampInSeconds"
"imu" -> "${context.getString(R.string.ccc_imu_link)}#t=$timestampInSeconds"
else -> ""
}
override suspend fun tryAndRescueError(item: String?, exception: PlaybackException): Uri? {
TODO("Not yet implemented") TODO("Not yet implemented")
} }
} }

View File

@ -40,7 +40,7 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<LinearLayout <LinearLayout
android:id="@+id/player_column" android:id="@+id/embedded_player_layout"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical" android:orientation="vertical"
@ -51,7 +51,7 @@
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<net.newpipe.newplayer.VideoPlayerView <net.newpipe.newplayer.VideoPlayerView
android:id="@+id/new_player_video_view" android:id="@+id/embedded_player"
android:name="net.newpipe.newplayer.VideoPlayerFragment" android:name="net.newpipe.newplayer.VideoPlayerFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -72,7 +72,7 @@
app:layout_constraintHorizontal_weight="1" app:layout_constraintHorizontal_weight="1"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/player_column" app:layout_constraintStart_toEndOf="@id/embedded_player_layout"
app:layout_constraintTop_toTopOf="parent" > app:layout_constraintTop_toTopOf="parent" >
<Button <Button

View File

@ -38,8 +38,8 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<FrameLayout <LinearLayout
android:id="@+id/player_column" android:id="@+id/embedded_player_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toTopOf="@+id/buttons_layout" app:layout_constraintBottom_toTopOf="@+id/buttons_layout"
@ -51,7 +51,7 @@
app:layout_constraintVertical_weight="1"> app:layout_constraintVertical_weight="1">
<net.newpipe.newplayer.VideoPlayerView <net.newpipe.newplayer.VideoPlayerView
android:id="@+id/new_player_video_view" android:id="@+id/embedded_player"
android:name="net.newpipe.newplayer.VideoPlayerFragment" android:name="net.newpipe.newplayer.VideoPlayerFragment"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
@ -61,7 +61,7 @@
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
</FrameLayout> </LinearLayout>
<FrameLayout <FrameLayout
android:id="@id/buttons_layout" android:id="@id/buttons_layout"
@ -69,7 +69,7 @@
android:layout_height="0dp" android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/player_column" app:layout_constraintTop_toBottomOf="@id/embedded_player_layout"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintVertical_weight="1"> app:layout_constraintVertical_weight="1">

View File

@ -20,22 +20,28 @@
--> -->
<resources> <resources>
<!-- "Reverse Engineering the MOS 6502 CPU" a talk from 27c3 -->
<string name="ccc_6502_link" translatable="false">https://media.ccc.de/v/27c3-4159-en-reverse_engineering_mos_6502</string>
<string name="ccc_6502_video" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4</string> <string name="ccc_6502_video" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/mp4-h264-HQ/27c3-4159-en-reverse_engineering_mos_6502.mp4</string>
<string name="ccc_6502_audio" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/ogg-audio-only/27c3-4159-en-reverse_engineering_mos_6502.ogg</string> <string name="ccc_6502_audio" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2010/ogg-audio-only/27c3-4159-en-reverse_engineering_mos_6502.ogg</string>
<string name="ccc_6502_title" translatable="false">Reverse Engineering the MOS 6502 CPU </string> <string name="ccc_6502_title" translatable="false">Reverse Engineering the MOS 6502 CPU </string>
<string name="ccc_6502_channel" translatable="false">Michael Steil</string> <string name="ccc_6502_channel" translatable="false">Michael Steil</string>
<string name="ccc_6402_thumbnail" translatable="false">https://static.media.ccc.de/media/congress/2010/27c3-4159-en-reverse_engineering_mos_6502.jpg</string> <string name="ccc_6502_thumbnail" translatable="false">https://static.media.ccc.de/media/congress/2010/27c3-4159-en-reverse_engineering_mos_6502.jpg</string>
<string name="ccc_6502_preview_thumbnails" translatable="false">https://cloud.newpipe-ev.de/remote.php/dav/public-files/YKGkfBlBNbkavqw/6502/onethirdsize/f000000%03d.jpg</string> <string name="ccc_6502_preview_thumbnails" translatable="false">https://cloud.newpipe-ev.de/remote.php/dav/public-files/YKGkfBlBNbkavqw/6502/onethirdsize/f000000%03d.jpg</string>
<string-array name="ccc_6502_chapters"> <integer-array name="ccc_6502_chapters">
<item>600000</item> <item>600000</item>
<item>1200000</item> <item>1200000</item>
<item>1800000</item> <item>1800000</item>
<item>2400000</item> <item>2400000</item>
<item>3600000</item> <item>3600000</item>
</string-array> </integer-array>
<!-- A thumbler Video. The creators tried to imitate an ai generated video. I found this in a Tom Scott newsletter. -->
<string name="portrait_video_example" translatable="false">https://va.media.tumblr.com/tumblr_sh62vjBX0j1z8ckep.mp4</string> <string name="portrait_video_example" translatable="false">https://va.media.tumblr.com/tumblr_sh62vjBX0j1z8ckep.mp4</string>
<!-- "Intel Management Engine deep dive" a talk from 36c3 -->
<string name ="ccc_imu_link" translatable="false">https://media.ccc.de/v/36c3-10694-intel_management_engine_deep_dive</string>
<string name="ccc_imu_1080_mp4" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/h264-hd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_hd.mp4</string> <string name="ccc_imu_1080_mp4" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/h264-hd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_hd.mp4</string>
<string name="ccc_imu_576_mp4" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/h264-sd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_sd.mp4</string> <string name="ccc_imu_576_mp4" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/h264-sd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_sd.mp4</string>
<string name="ccc_imu_1080_webm" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/webm-hd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_webm-hd.webm</string> <string name="ccc_imu_1080_webm" translatable="false">https://ftp.fau.de/cdn.media.ccc.de/congress/2019/webm-hd/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive_webm-hd.webm</string>
@ -46,12 +52,12 @@
<string name="ccc_imu_channel" translatable="false">Peter Bosch</string> <string name="ccc_imu_channel" translatable="false">Peter Bosch</string>
<string name="ccc_imu_thumbnail" translatable="false">https://static.media.ccc.de/media/congress/2019/10694-hd.jpg</string> <string name="ccc_imu_thumbnail" translatable="false">https://static.media.ccc.de/media/congress/2019/10694-hd.jpg</string>
<string name="ccc_imu_preview_thumbnails" translatable="false">https://cloud.newpipe-ev.de/remote.php/dav/public-files/YKGkfBlBNbkavqw/intel_mu/oneeigthsize/f000000%03d.jpg</string> <string name="ccc_imu_preview_thumbnails" translatable="false">https://cloud.newpipe-ev.de/remote.php/dav/public-files/YKGkfBlBNbkavqw/intel_mu/oneeigthsize/f000000%03d.jpg</string>
<string-array name="ccc_imu_chapters"> <integer-array name="ccc_imu_chapters">
<item>600000</item> <item>600000</item>
<item>1200000</item> <item>1200000</item>
<item>1800000</item> <item>1800000</item>
<item>2400000</item> <item>2400000</item>
<item>3600000</item> <item>3600000</item>
</string-array> </integer-array>
<string name="ccc_imu_subtitles" translatable="false">https://cdn.media.ccc.de/congress/2019/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive.en.srt</string> <string name="ccc_imu_subtitles" translatable="false">https://cdn.media.ccc.de/congress/2019/36c3-10694-eng-deu-Intel_Management_Engine_deep_dive.en.srt</string>
</resources> </resources>