diff --git a/app/build.gradle b/app/build.gradle index 04e20aef1..8e6c40c26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -229,6 +229,9 @@ dependencies { implementation libs.androidx.work.runtime implementation libs.androidx.work.rxjava3 implementation libs.androidx.material + implementation libs.androidx.media3.common + implementation libs.androidx.media3.exoplayer + implementation libs.androidx.media3.ui /** Third-party libraries **/ // Instance state boilerplate elimination diff --git a/app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt b/app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt index 77dcbdc4c..fa2cde676 100644 --- a/app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt +++ b/app/src/main/java/org/schabi/newpipe/NewPlayerComponent.kt @@ -21,8 +21,14 @@ package net.newpipe.newplayer.testapp import android.app.Application +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri import android.util.Log +import androidx.annotation.OptIn import androidx.core.graphics.drawable.IconCompat +import androidx.media3.common.MediaMetadata +import androidx.media3.common.util.UnstableApi import dagger.Module import dagger.Provides import dagger.hilt.InstallIn @@ -30,11 +36,19 @@ import dagger.hilt.components.SingletonComponent import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.newpipe.newplayer.NewPlayer import net.newpipe.newplayer.NewPlayerImpl +import net.newpipe.newplayer.data.AudioStreamTrack +import net.newpipe.newplayer.data.Chapter +import net.newpipe.newplayer.data.Stream +import net.newpipe.newplayer.data.Subtitle +import net.newpipe.newplayer.data.VideoStreamTrack import net.newpipe.newplayer.repository.CachingRepository -import net.newpipe.newplayer.repository.PlaceHolderRepository +import net.newpipe.newplayer.repository.MediaRepository import net.newpipe.newplayer.repository.PrefetchingRepository +import okhttp3.OkHttpClient +import okhttp3.Request import org.schabi.newpipe.App import org.schabi.newpipe.MainActivity import javax.inject.Singleton @@ -47,7 +61,7 @@ object NewPlayerComponent { fun provideNewPlayer(app: Application): NewPlayer { val player = NewPlayerImpl( app = app, - repository = PrefetchingRepository(CachingRepository(PlaceHolderRepository())), + repository = PrefetchingRepository(CachingRepository(TestMediaRepository())), notificationIcon = IconCompat.createWithResource(app, net.newpipe.newplayer.R.drawable.new_player_tiny_icon), playerActivityClass = MainActivity::class.java, // rescueStreamFault = … @@ -64,3 +78,83 @@ object NewPlayerComponent { return player } } + +class TestMediaRepository() : MediaRepository { + private val client = OkHttpClient() + + override fun getRepoInfo() = + MediaRepository.RepoMetaInfo(canHandleTimestampedLinks = true, pullsDataFromNetwork = true) + + @OptIn(UnstableApi::class) + override suspend fun getMetaInfo(item: String): MediaMetadata = + MediaMetadata.Builder() + .setTitle("BGP and the rule of bla") + .setArtist("mr BGP") + .setArtworkUri(Uri.parse("https://static.media.ccc.de/media/congress/2017/9072-hd.jpg")) + .setDurationMs( + 1871L * 1000L + ) + .build() + + override suspend fun getStreams(item: String): List { + return listOf( + Stream( + item = "bgp", + streamUri = Uri.parse("https://cdn.media.ccc.de/congress/2017/h264-hd/34c3-9072-eng-BGP_and_the_Rule_of_Custom.mp4"), + mimeType = null, + streamTracks = listOf( + AudioStreamTrack( + bitrate = 480000, + fileFormat = "MPEG4", + language = "en" + ), + VideoStreamTrack( + width = 1920, + height = 1080, + frameRate = 25, + fileFormat = "MPEG4" + ) + ) + ) + ) + } + + override suspend fun getSubtitles(item: String) = + emptyList() + + override suspend fun getPreviewThumbnail(item: String, timestampInMs: Long): Bitmap? { + + val templateUrl = "https://static.media.ccc.de/media/congress/2017/9072-hd.jpg" + + 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(0, 0) + + override suspend fun getChapters(item: String) = + listOf() + + override suspend fun getTimestampLink(item: String, timestampInSeconds: Long) = + "" +} diff --git a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java index 63077e92d..196b9734b 100644 --- a/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java +++ b/app/src/main/java/org/schabi/newpipe/fragments/detail/VideoDetailFragment.java @@ -63,6 +63,9 @@ import com.google.android.material.appbar.AppBarLayout; import com.google.android.material.bottomsheet.BottomSheetBehavior; import com.google.android.material.tabs.TabLayout; +import net.newpipe.newplayer.NewPlayer; +import net.newpipe.newplayer.data.PlayMode; + import org.schabi.newpipe.App; import org.schabi.newpipe.R; import org.schabi.newpipe.database.stream.model.StreamEntity; @@ -128,11 +131,15 @@ import java.util.concurrent.TimeUnit; import java.util.function.Consumer; import coil3.util.CoilUtils; +import javax.inject.Inject; + +import dagger.hilt.android.AndroidEntryPoint; import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.CompositeDisposable; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; +@AndroidEntryPoint public final class VideoDetailFragment extends BaseStateFragment implements BackPressable, @@ -228,6 +235,8 @@ public final class VideoDetailFragment @Nullable private PlayerService playerService; private Player player; + @Inject + NewPlayer newPlayer; private final PlayerHolder playerHolder = PlayerHolder.getInstance(); /*////////////////////////////////////////////////////////////////////////// @@ -1138,10 +1147,13 @@ public final class VideoDetailFragment final PlayQueue queue = setupPlayQueueForIntent(false); tryAddVideoPlayerView(); + newPlayer.playStream("bgp", PlayMode.EMBEDDED_VIDEO); + newPlayer.setPlayWhenReady(true); + newPlayer.prepare(); - final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), - PlayerService.class, queue, true, autoPlayEnabled); - ContextCompat.startForegroundService(activity, playerIntent); +// final Intent playerIntent = NavigationHelper.getPlayerIntent(requireContext(), +// PlayerService.class, queue, true, autoPlayEnabled); +// ContextCompat.startForegroundService(activity, playerIntent); } /** @@ -1234,15 +1246,18 @@ public final class VideoDetailFragment // setup the surface view height, so that it fits the video correctly setHeightThumbnail(); - player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { - // sometimes binding would be null here, even though getView() != null above u.u - if (binding != null) { - // prevent from re-adding a view multiple times - playerUi.removeViewFromParent(); - binding.playerPlaceholder.addView(playerUi.getBinding().getRoot()); - playerUi.setupVideoSurfaceIfNeeded(); - } - }); + getLayoutInflater().inflate( + R.layout.fragment_newplayer_view, binding.playerPlaceholder); + +// player.UIs().get(MainPlayerUi.class).ifPresent(playerUi -> { +// // sometimes binding would be null here, even though getView() != null above u.u +// if (binding != null) { +// // prevent from re-adding a view multiple times +// playerUi.removeViewFromParent(); +// binding.playerPlaceholder.addView(playerUi.getBinding().getRoot()); +// playerUi.setupVideoSurfaceIfNeeded(); +// } +// }); }); } diff --git a/app/src/main/res/layout/fragment_newplayer_view.xml b/app/src/main/res/layout/fragment_newplayer_view.xml new file mode 100644 index 000000000..a3a00d91d --- /dev/null +++ b/app/src/main/res/layout/fragment_newplayer_view.xml @@ -0,0 +1,7 @@ + + diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5dc0379d..004974388 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,6 +32,7 @@ localbroadcastmanager = "1.1.0" markwon = "4.6.2" material = "1.11.0" media = "1.7.0" +media3 = "1.3.1" mockitoCore = "5.6.0" navigationCompose = "2.8.3" okhttp = "4.12.0" @@ -96,6 +97,9 @@ androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "l androidx-localbroadcastmanager = { group = "androidx.localbroadcastmanager", name = "localbroadcastmanager", version.ref = "localbroadcastmanager" } androidx-material = { group = "com.google.android.material", name = "material", version.ref = "material" } androidx-media = { group = "androidx.media", name = "media", version.ref = "media" } +androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "media3" } +androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "media3" } +androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "media3" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } androidx-paging-compose = { group = "androidx.paging", name = "paging-compose", version.ref = "pagingCompose" } androidx-preference = { group = "androidx.preference", name = "preference", version.ref = "preference" }