Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -227,12 +227,13 @@ dependencies {

/** Kotlin **/
implementation "org.jetbrains.kotlin:kotlin-stdlib"
implementation 'androidx.compose.animation:animation:1.3.1'
implementation 'androidx.compose.foundation:foundation:1.3.1'
implementation 'androidx.compose.material3:material3:1.0.1'
implementation 'androidx.compose.material:material-icons-extended:1.3.1'
implementation 'androidx.compose.ui:ui:1.3.1'
implementation 'androidx.compose.ui:ui-tooling-preview:1.3.1'
implementation platform('androidx.compose:compose-bom:2025.08.01')
implementation 'androidx.compose.animation:animation'
implementation 'androidx.compose.foundation:foundation'
implementation 'androidx.compose.material3:material3'
implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.compose.ui:ui'
implementation 'androidx.compose.ui:ui-tooling-preview'

/** AndroidX **/
implementation 'androidx.appcompat:appcompat:1.3.1'
Expand Down Expand Up @@ -273,6 +274,13 @@ dependencies {
// Media player
implementation "com.google.android.exoplayer:exoplayer:${exoPlayerVersion}"
implementation "com.google.android.exoplayer:extension-mediasession:${exoPlayerVersion}"
implementation "androidx.media3:media3-session:1.8.0"
implementation "androidx.media3:media3-exoplayer:1.8.0"
implementation "androidx.media3:media3-ui:1.8.0"
implementation "androidx.media3:media3-common:1.8.0"
implementation "androidx.media3:media3-datasource:1.8.0"
implementation "androidx.media3:media3-exoplayer-dash:1.8.0"
implementation "androidx.media3:media3-exoplayer-hls:1.8.0"

// Metadata generator for service descriptors
compileOnly "com.google.auto.service:auto-service-annotations:${googleAutoServiceVersion}"
Expand Down
10 changes: 10 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@
android:foregroundServiceType="mediaPlayback">
</service>

<service
android:name="project.pipepipe.app.service.PlaybackService"
android:exported="true"
android:foregroundServiceType="mediaPlayback">
<intent-filter>
<action android:name="androidx.media3.session.MediaLibraryService" />
<action android:name="android.media.browse.MediaBrowserService" />
</intent-filter>
</service>
Comment on lines +78 to +86

<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/org/schabi/newpipe/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,8 @@
import javax.net.ssl.SSLSession;
import javax.net.ssl.X509TrustManager;

import project.pipepipe.app.ui.ExperimentalVideoDetailHost;

public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
@SuppressWarnings("ConstantConditions")
Expand Down Expand Up @@ -151,6 +153,7 @@ protected void onCreate(final Bundle savedInstanceState) {
.getHeaderView(0));
toolbarLayoutBinding = mainBinding.toolbarLayout;
setContentView(mainBinding.getRoot());
ExperimentalVideoDetailHost.attach(this, mainBinding.experimentalVideoDetailHost);

if (getSupportFragmentManager().getBackStackEntryCount() == 0) {
initFragments();
Expand Down Expand Up @@ -617,6 +620,10 @@ public boolean onKeyDown(final int keyCode, final KeyEvent event) {

@Override
public void onBackPressed() {
if (ThemeHelper.shouldUseExperimentalNewUi(this)
&& ExperimentalVideoDetailHost.onBackPressed()) {
return;
}
if (DEBUG) {
Log.d(TAG, "onBackPressed() called");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import androidx.car.app.connection.CarConnection;
import org.schabi.newpipe.player.PlayerBinderInterface;
import org.schabi.newpipe.player.mediasession.PlayerServiceInterface;
import project.pipepipe.app.service.PlaybackService;

public class CarConnectionStateReceiver extends BroadcastReceiver {

Expand Down Expand Up @@ -39,6 +40,10 @@ public static void setCarConnectionState(boolean connected) {
}

private static void shutdownOldService(Context context) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
context.stopService(new Intent(context, PlaybackService.class));
return;
}
ServiceConnection connection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public class MediaButtonReceiver extends BroadcastReceiver {

@Override
public void onReceive(Context context, Intent intent) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
return;
}
// We only care about the MEDIA_BUTTON intent.
if (Intent.ACTION_MEDIA_BUTTON.equals(intent.getAction())) {
KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);
Expand All @@ -27,4 +30,4 @@ public void onReceive(Context context, Intent intent) {
}
}
}
}
}
49 changes: 49 additions & 0 deletions app/src/main/java/org/schabi/newpipe/util/NavigationHelper.java
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

import com.jakewharton.processphoenix.ProcessPhoenix;

import org.schabi.newpipe.App;
import org.schabi.newpipe.MainActivity;
import org.schabi.newpipe.NewPipeDatabase;
import org.schabi.newpipe.R;
Expand Down Expand Up @@ -61,6 +62,10 @@
import org.schabi.newpipe.settings.SettingsActivity;
import org.schabi.newpipe.util.external_communication.ShareUtils;

import project.pipepipe.app.ui.ExperimentalVideoDetailHost;
import project.pipepipe.app.ExperimentalPlaybackRouter;
import project.pipepipe.app.PlaybackMode;

import java.util.ArrayList;
import java.util.Random;

Expand Down Expand Up @@ -135,6 +140,12 @@ public static void playOnMainPlayer(final AppCompatActivity activity,
@NonNull final PlayQueue playQueue) {
final PlayQueueItem item = playQueue.getItem();
if (item != null) {
if (ThemeHelper.shouldUseExperimentalNewUi(activity)) {
ExperimentalPlaybackRouter.play(
activity, playQueue, PlaybackMode.VIDEO_AUDIO, false);
ExperimentalVideoDetailHost.open(item.getServiceId(), item.getUrl());
return;
}
openVideoDetailFragment(activity, activity.getSupportFragmentManager(),
item.getServiceId(), item.getUrl(), item.getTitle(), playQueue,
false);
Expand All @@ -146,6 +157,12 @@ public static void playOnMainPlayer(final Context context,
final boolean switchingPlayers) {
final PlayQueueItem item = playQueue.getItem();
if (item != null) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.play(
context, playQueue, PlaybackMode.VIDEO_AUDIO, false);
ExperimentalVideoDetailHost.open(item.getServiceId(), item.getUrl());
return;
}
openVideoDetail(context,
item.getServiceId(), item.getUrl(), item.getTitle(), playQueue,
switchingPlayers);
Expand All @@ -159,6 +176,10 @@ public static void playOnPopupPlayer(final Context context,
PermissionHelper.showPopupEnablementToast(context);
return;
}
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.play(context, queue, PlaybackMode.POPUP, false);
return;
}

Toast.makeText(context, R.string.popup_playing_toast, Toast.LENGTH_SHORT).show();

Expand All @@ -170,6 +191,10 @@ public static void playOnPopupPlayer(final Context context,
public static void playOnBackgroundPlayer(final Context context,
final PlayQueue queue,
final boolean resumePlayback) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.play(context, queue, PlaybackMode.AUDIO_ONLY, false);
return;
}
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show();

Expand All @@ -181,6 +206,10 @@ public static void playOnBackgroundPlayer(final Context context,
public static void playOnBackgroundPlayerShuffled(final Context context,
final PlayQueue queue,
final boolean resumePlayback) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.play(context, queue, PlaybackMode.AUDIO_ONLY, true);
return;
}
Toast.makeText(context, R.string.background_player_playing_toast, Toast.LENGTH_SHORT)
.show();
queue.setIndex(new Random().nextInt(queue.getStreams().size()));
Expand All @@ -199,6 +228,10 @@ public static void enqueueOnPlayer(final Context context,
PermissionHelper.showPopupEnablementToast(context);
return;
}
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.enqueue(context, queue, false);
return;
}

Toast.makeText(context, R.string.enqueued, Toast.LENGTH_SHORT).show();
final Intent intent = getPlayerEnqueueIntent(context, DeviceUtils.getPlayerServiceClass(), queue);
Expand All @@ -219,6 +252,10 @@ public static void enqueueOnPlayer(final Context context, final PlayQueue queue)

/* ENQUEUE NEXT */
public static void enqueueNextOnPlayer(final Context context, final PlayQueue queue) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.enqueue(context, queue, true);
return;
}
PlayerType playerType = PlayerHolder.getInstance().getType();
if (!PlayerHolder.getInstance().isPlayerOpen()) {
Log.e(TAG, "Enqueueing next but no player is open; defaulting to background player");
Expand Down Expand Up @@ -347,6 +384,10 @@ public static void openSearchFragment(final FragmentManager fragmentManager,
}

public static void expandMainPlayer(final Context context) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalVideoDetailHost.expand();
return;
}
context.sendBroadcast(new Intent(VideoDetailFragment.ACTION_SHOW_MAIN_PLAYER)
.setPackage(context.getPackageName()));
}
Expand All @@ -357,6 +398,10 @@ public static void sendPlayerStartedEvent(final Context context) {
}

public static void showMiniPlayer(final FragmentManager fragmentManager) {
if (ThemeHelper.shouldUseExperimentalNewUi(fragmentManager.getFragments().isEmpty()
? App.getApp() : fragmentManager.getFragments().get(0).requireContext())) {
return;
}
final VideoDetailFragment instance = VideoDetailFragment.getInstanceInCollapsedState();
defaultTransaction(fragmentManager)
.replace(R.id.fragment_player_holder, instance)
Expand All @@ -375,6 +420,10 @@ public static void openVideoDetailFragment(@NonNull final Context context,
@NonNull final String title,
@Nullable final PlayQueue playQueue,
final boolean switchingPlayers) {
if (ThemeHelper.shouldUseExperimentalNewUi(context) && url != null) {
ExperimentalVideoDetailHost.open(serviceId, url);
return;
}

final boolean autoPlay;
@Nullable final PlayerService.PlayerType playerType = PlayerHolder.getInstance().getType();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@
import org.schabi.newpipe.player.playqueue.SinglePlayQueue;
import org.schabi.newpipe.util.ExtractorHelper;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;

import project.pipepipe.app.ExperimentalPlaybackRouter;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -100,8 +103,13 @@ private static boolean handleTimestampUrl(final Context context,
@NonNull final Pattern pattern,
@NonNull final CompositeDisposable disposables) {
if(url.contains("internal://timestamp/")) {
final int timestamp = Integer.parseInt(url.split("internal://timestamp/")[1]);
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.seekTo(timestamp * 1000L);
return true;
}
Intent intent = new Intent(ACTION_SEEK_TO);
intent.putExtra("Timestamp", Integer.parseInt(url.split("internal://timestamp/")[1]));
intent.putExtra("Timestamp", timestamp);
context.sendBroadcast(intent);
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
import org.schabi.newpipe.extractor.Info;
import org.schabi.newpipe.extractor.stream.StreamInfo;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ThemeHelper;

import project.pipepipe.app.ExperimentalPlaybackRouter;

import java.util.regex.Matcher;
import java.util.regex.Pattern;
Expand Down Expand Up @@ -193,6 +196,10 @@ private static void addClickListenersOnTimestamps(final Context context,
new ClickableSpan() {
@Override
public void onClick(@NonNull final View view) {
if (ThemeHelper.shouldUseExperimentalNewUi(context)) {
ExperimentalPlaybackRouter.seekTo(timestampMatchDTO.seconds() * 1000L);
return;
}
Intent intent = new Intent(ACTION_SEEK_TO);
intent.putExtra("Timestamp", timestampMatchDTO.seconds());
context.sendBroadcast(intent);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package project.pipepipe.app

import android.content.Context
import android.content.Intent
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.schabi.newpipe.player.playqueue.PlayQueue
import project.pipepipe.app.service.PlaybackService

object ExperimentalPlaybackRouter {
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())

@JvmStatic
fun play(context: Context, queue: PlayQueue, mode: PlaybackMode, shuffle: Boolean) {
context.startService(Intent(context, PlaybackService::class.java))
scope.launch {
val controller = awaitController() ?: return@launch
val items = LegacyPlayQueueAdapter.convert(queue)
SharedContext.updatePlaybackMode(mode)
SharedContext.queueManager.setQueue(items, queue.index)
if (shuffle) {
SharedContext.queueManager.shuffle(items.getOrNull(queue.index)?.uuid)
}
controller.prepare()
controller.play()
if (mode == PlaybackMode.POPUP) {
context.startService(
Intent(context, PlaybackService::class.java)
.setAction(PlaybackService.ACTION_SHOW_POPUP)
)
}
}
}

@JvmStatic
fun enqueue(context: Context, queue: PlayQueue, next: Boolean) {
context.startService(Intent(context, PlaybackService::class.java))
scope.launch {
val controller = awaitController() ?: return@launch
val items = LegacyPlayQueueAdapter.convert(queue)
if (SharedContext.queueManager.getCurrentQueue().isEmpty()) {
SharedContext.queueManager.setQueue(items, queue.index)
controller.prepare()
return@launch
}
items.forEach(SharedContext.queueManager::addItem)
if (next) {
val currentIndex = controller.currentItemIndex.value
items.forEachIndexed { offset, item ->
val from = SharedContext.queueManager.getIndexOfItemUuid(item.uuid)
SharedContext.queueManager.moveItem(from, currentIndex + 1 + offset)
}
}
}
}

@JvmStatic
fun seekTo(positionMs: Long) {
SharedContext.platformMediaController?.seekTo(positionMs)
}

private suspend fun awaitController(): project.pipepipe.app.platform.PlatformMediaController? {
repeat(50) {
SharedContext.platformMediaController?.let { return it }
delay(100)
}
return null
}
}
17 changes: 17 additions & 0 deletions app/src/main/java/project/pipepipe/app/LegacyPlayQueueAdapter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package project.pipepipe.app

import org.schabi.newpipe.player.playqueue.PlayQueue
import project.pipepipe.app.platform.PlatformMediaItem

object LegacyPlayQueueAdapter {
fun convert(queue: PlayQueue): List<PlatformMediaItem> = queue.streams.map {
PlatformMediaItem(
mediaId = it.url,
title = it.title,
artist = it.uploader,
artworkUrl = it.thumbnailUrl,
durationMs = it.duration.takeIf { duration -> duration > 0 }?.times(1000),
serviceId = it.serviceId
)
}
}
7 changes: 7 additions & 0 deletions app/src/main/java/project/pipepipe/app/PlaybackMode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package project.pipepipe.app

enum class PlaybackMode {
VIDEO_AUDIO,
AUDIO_ONLY,
POPUP
}
Loading