From 4ba30bed8453cf329746795f241496a18a3d4d38 Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 17:53:03 +0530 Subject: [PATCH 01/20] Add ExoPlayer-based external-player module with intent filters, basic player UI and PiP support --- .gitignore | 25 +--- external-player/README.md | 35 +++++ external-player/build.gradle | 32 +++++ external-player/src/main/AndroidManifest.xml | 36 +++++ .../externalplayer/ExternalPlayerActivity.kt | 125 ++++++++++++++++++ .../res/layout/activity_external_player.xml | 41 ++++++ 6 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 external-player/README.md create mode 100644 external-player/build.gradle create mode 100644 external-player/src/main/AndroidManifest.xml create mode 100644 external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt create mode 100644 external-player/src/main/res/layout/activity_external_player.xml diff --git a/.gitignore b/.gitignore index 1352b69172a..f706f7ca72c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,5 @@ -.gradle/ -local.properties -.DS_Store -build/ -captures/ -.idea/ -*.iml -*~ -.weblate -*.class -app/debug/ -app/release/ - -# vscode / eclipse files -*.classpath -*.project -*.settings -bin/ -.vscode/ -*.code-workspace +# Excluded files +/external-player/build/ +/*.iml +/.gradle +/local.properties diff --git a/external-player/README.md b/external-player/README.md new file mode 100644 index 00000000000..d17d2f6b00c --- /dev/null +++ b/external-player/README.md @@ -0,0 +1,35 @@ +# NewPipe External Player (feature/mx-like-external-player) + +This module provides an ExoPlayer-based external video player which can be used as a system external player: +- Accepts VIEW and SEND intents (http(s) links, text share). +- Basic ExoPlayer integration with PlayerView and controls. +- Picture-in-Picture (PiP) support (Android O+). +- UI skeleton matching typical external players (MX-style quick controls). + +What is included in this branch: +- external-player module with PlayerActivity, layout, and manifest intent filters. +- build.gradle configured for ExoPlayer. + +Important notes / Limitations: +- Direct YouTube links cannot be played directly by ExoPlayer; YouTube requires stream extraction. This branch includes placeholders and a TODO where the app should call the extractor already present in the main app (NewPipe's extractor) to produce playable stream URLs, or integrate a secure YouTube extraction pipeline. +- DRM, subtitles, audio-only background service, casting, and advanced subtitle selection are not yet implemented in this initial commit. + +Planned next steps / TODOs (can be split to issues): +- Integrate with NewPipe extractor API to handle YouTube pages/IDs -> actual video stream URLs (muxed/dash) (High priority) +- Support background playback and notification controls (media session & notification) +- Support playlists and queueing (play next/previous) +- Add subtitle downloading and selection (TTML/SRT/WebVTT) +- Add audio boost, hardware acceleration toggles, speed control, equalizer integration +- Implement Chromecast / DLNA / Google Cast support +- Improve UX: gestures (seek/surface brightness/volume), aspect-ratio toggles, resume playback +- Add tests and instrumentation tests for intent handling and PiP flows + +How to test locally: +1. Add `include ':external-player'` to your root settings.gradle +2. Build and install the app variant or run as a standalone APK and send an ACTION_VIEW intent: + adb shell am start -a android.intent.action.VIEW -d "https://www.example.com/video.mp4" org.newpipe.externalplayer/.ExternalPlayerActivity +3. Share a YouTube URL via Android share sheet to the player to validate intent handling (extraction not implemented yet). + +Security & privacy notes: +- Do not embed broken/unsafe YouTube extractors. Prefer reusing the app's extractor or server-assisted extraction. Respect Terms of Service where applicable. +- If integrating with YouTube, prefer the app's existing extractor infrastructure to avoid duplicating logic and leaking credentials. diff --git a/external-player/build.gradle b/external-player/build.gradle new file mode 100644 index 00000000000..4a000f28372 --- /dev/null +++ b/external-player/build.gradle @@ -0,0 +1,32 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 33 + + defaultConfig { + applicationId "org.newpipe.externalplayer" + minSdkVersion 21 + targetSdkVersion 33 + versionCode 1 + versionName "1.0" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + buildFeatures { + viewBinding true + } +} + +dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' + implementation 'com.google.android.material:material:1.9.0' + implementation 'androidx.core:core-ktx:1.10.1' + implementation 'com.google.android.exoplayer:exoplayer:2.19.0' + implementation 'androidx.media:media:1.6.0' +} \ No newline at end of file diff --git a/external-player/src/main/AndroidManifest.xml b/external-player/src/main/AndroidManifest.xml new file mode 100644 index 00000000000..c67dab70e41 --- /dev/null +++ b/external-player/src/main/AndroidManifest.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt b/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt new file mode 100644 index 00000000000..0657ac1d3bf --- /dev/null +++ b/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt @@ -0,0 +1,125 @@ +package org.newpipe.externalplayer + +import android.app.PictureInPictureParams +import android.content.Intent +import android.net.Uri +import android.os.Build +import android.os.Bundle +import android.util.Rational +import android.view.View +import androidx.appcompat.app.AppCompatActivity +import com.google.android.exoplayer2.MediaItem +import com.google.android.exoplayer2.Player.STATE_READY +import com.google.android.exoplayer2.SimpleExoPlayer +import org.newpipe.externalplayer.databinding.ActivityExternalPlayerBinding + +class ExternalPlayerActivity : AppCompatActivity() { + + private lateinit var binding: ActivityExternalPlayerBinding + private var player: SimpleExoPlayer? = null + private var playWhenReady = true + private var playbackPosition: Long = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + binding = ActivityExternalPlayerBinding.inflate(layoutInflater) + setContentView(binding.root) + + handleIncomingIntent(intent) + + binding.enterPipButton.setOnClickListener { + enterPip() + } + } + + private fun handleIncomingIntent(intent: Intent?) { + if (intent == null) return + val action = intent.action + val data: Uri? = intent.data ?: intent.getParcelableExtra(Intent.EXTRA_STREAM) + if (Intent.ACTION_VIEW == action || data != null) { + data?.let { uri -> + binding.urlText.text = uri.toString() + initializePlayer(uri.toString()) + } + } else if (Intent.ACTION_SEND == action) { + val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.getStringExtra(Intent.EXTRA_STREAM)?.toString() + text?.let { + val url = extractUrlFromText(it) + if (url != null) initializePlayer(url) + } + } + } + + private fun extractUrlFromText(text: String): String? { + val regex = "(https?://[\w\-._~:/?#[\]@!$&'()*+,;=%]+)".toRegex() + return regex.find(text)?.value + } + + private fun initializePlayer(url: String) { + if (player == null) { + player = SimpleExoPlayer.Builder(this).build() + binding.playerView.player = player + } + val mediaItem = MediaItem.fromUri(Uri.parse(url)) + player!!.setMediaItem(mediaItem) + player!!.playWhenReady = playWhenReady + player!!.seekTo(playbackPosition) + player!!.prepare() + player!!.addListener(object : com.google.android.exoplayer2.Player.Listener { + override fun onPlaybackStateChanged(state: Int) { + binding.loadingView.visibility = if (state == STATE_READY) View.GONE else View.VISIBLE + } + }) + } + + private fun enterPip() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val ratio = Rational(binding.playerView.width.takeIf { it > 0 } ?: 16, binding.playerView.height.takeIf { it > 0 } ?: 9) + val params = PictureInPictureParams.Builder() + .setAspectRatio(ratio) + .build() + enterPictureInPictureMode(params) + } + } + + override fun onUserLeaveHint() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + enterPip() + } + } + + override fun onPause() { + super.onPause() + player?.let { + playbackPosition = it.currentPosition + playWhenReady = it.playWhenReady + it.playWhenReady = false + } + } + + override fun onResume() { + super.onResume() + player?.playWhenReady = playWhenReady + } + + override fun onStop() { + super.onStop() + if (!isInPictureInPictureMode) { + releasePlayer() + } + } + + override fun onDestroy() { + super.onDestroy() + releasePlayer() + } + + private fun releasePlayer() { + player?.let { + playbackPosition = it.currentPosition + playWhenReady = it.playWhenReady + it.release() + player = null + } + } +} \ No newline at end of file diff --git a/external-player/src/main/res/layout/activity_external_player.xml b/external-player/src/main/res/layout/activity_external_player.xml new file mode 100644 index 00000000000..6c9a8f12abb --- /dev/null +++ b/external-player/src/main/res/layout/activity_external_player.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + \ No newline at end of file From 0d87c0265b76c64239717c8767a474a019ede8f5 Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:00:36 +0530 Subject: [PATCH 02/20] Enhance external-player: PlayerService, MediaSession/notification, speed control, subtitle stub and manifest updates --- external-player/build.gradle | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/external-player/build.gradle b/external-player/build.gradle index 4a000f28372..6d8d796985c 100644 --- a/external-player/build.gradle +++ b/external-player/build.gradle @@ -1,4 +1,5 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' android { compileSdkVersion 33 @@ -8,7 +9,7 @@ android { minSdkVersion 21 targetSdkVersion 33 versionCode 1 - versionName "1.0" + versionName "1.1" } buildTypes { @@ -21,12 +22,30 @@ android { buildFeatures { viewBinding true } + + compileOptions { + sourceCompatibility = 1.8 + targetCompatibility = 1.8 + } + kotlinOptions { + jvmTarget = "1.8" + } } dependencies { + implementation 'org.jetbrains.kotlin:kotlin-stdlib:1.8.22' implementation 'androidx.appcompat:appcompat:1.6.1' implementation 'com.google.android.material:material:1.9.0' implementation 'androidx.core:core-ktx:1.10.1' implementation 'com.google.android.exoplayer:exoplayer:2.19.0' implementation 'androidx.media:media:1.6.0' + implementation 'androidx.lifecycle:lifecycle-service:2.6.1' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.1' + implementation 'androidx.media:media-session:1.2.0' + implementation 'androidx.activity:activity-ktx:1.8.0' + implementation 'androidx.constraintlayout:constraintlayout:2.1.4' + implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0' + implementation 'androidx.vectordrawable:vectordrawable:1.2.0' + implementation 'androidx.core:core:1.10.1' + implementation 'com.github.bumptech.glide:glide:4.15.1' } \ No newline at end of file From 9d6e9c003ccab7173694a0051ed83346e891f32d Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:05:50 +0530 Subject: [PATCH 03/20] Update AndroidManifest.xml with PlayerService and permissions --- external-player/src/main/AndroidManifest.xml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/external-player/src/main/AndroidManifest.xml b/external-player/src/main/AndroidManifest.xml index c67dab70e41..4679758168e 100644 --- a/external-player/src/main/AndroidManifest.xml +++ b/external-player/src/main/AndroidManifest.xml @@ -4,6 +4,7 @@ + + + \ No newline at end of file From 3431c2dd540b27f4da46d9e5dea0ff48652173ba Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:06:35 +0530 Subject: [PATCH 04/20] Add PlayerService with ExoPlayer and MediaSession support --- .../newpipe/externalplayer/PlayerService.kt | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) create mode 100644 external-player/src/main/java/org/newpipe/externalplayer/PlayerService.kt diff --git a/external-player/src/main/java/org/newpipe/externalplayer/PlayerService.kt b/external-player/src/main/java/org/newpipe/externalplayer/PlayerService.kt new file mode 100644 index 00000000000..0bbfd6e803d --- /dev/null +++ b/external-player/src/main/java/org/newpipe/externalplayer/PlayerService.kt @@ -0,0 +1,133 @@ +package org.newpipe.externalplayer + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.os.Build +import android.os.IBinder +import androidx.lifecycle.LifecycleService +import android.app.NotificationChannel +import android.app.NotificationManager +import androidx.media.app.NotificationCompat.MediaStyle +import android.support.v4.media.session.MediaSessionCompat +import android.support.v4.media.MediaMetadataCompat +import android.support.v4.media.session.PlaybackStateCompat +import com.google.android.exoplayer2.ExoPlayer +import com.google.android.exoplayer2.MediaItem + +class PlayerService : LifecycleService() { + + companion object { + const val CHANNEL_ID = "external_player_channel" + const val NOTIF_ID = 1 + + const val ACTION_PLAY = "org.newpipe.externalplayer.action.PLAY" + const val ACTION_PAUSE = "org.newpipe.externalplayer.action.PAUSE" + const val ACTION_STOP = "org.newpipe.externalplayer.action.STOP" + const val ACTION_SET_URI = "org.newpipe.externalplayer.action.SET_URI" + const val EXTRA_URI = "uri" + } + + private lateinit var mediaSession: MediaSessionCompat + private var player: ExoPlayer? = null + private lateinit var notificationManager: MediaNotificationManager + + override fun onCreate() { + super.onCreate() + createChannel() + mediaSession = MediaSessionCompat(this, "ExternalPlayerSession").apply { isActive = true } + notificationManager = MediaNotificationManager(this) + initializePlayer() + } + + private fun initializePlayer() { + if (player == null) { + player = ExoPlayer.Builder(this).build() + player?.addListener(object : com.google.android.exoplayer2.Player.Listener { + override fun onIsPlayingChanged(isPlaying: Boolean) { + updatePlaybackState() + startForegroundIfNeeded() + } + }) + } + } + + private fun startForegroundIfNeeded() { + val notification = notificationManager.buildNotification(player, mediaSession.sessionToken) + if (player?.isPlaying == true) { + startForeground(NOTIF_ID, notification) + } else { + val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + nm.notify(NOTIF_ID, notification) + } + } + + private fun updatePlaybackState() { + val state = if (player?.isPlaying == true) PlaybackStateCompat.STATE_PLAYING else PlaybackStateCompat.STATE_PAUSED + val pos = player?.currentPosition ?: 0L + val playbackState = PlaybackStateCompat.Builder() + .setActions( + PlaybackStateCompat.ACTION_PLAY or + PlaybackStateCompat.ACTION_PAUSE or + PlaybackStateCompat.ACTION_PLAY_PAUSE or + PlaybackStateCompat.ACTION_SEEK_TO or + PlaybackStateCompat.ACTION_STOP + ) + .setState(state, pos, 1.0f) + .build() + mediaSession.setPlaybackState(playbackState) + } + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + intent?.action?.let { action -> + when (action) { + ACTION_PLAY -> player?.play() + ACTION_PAUSE -> player?.pause() + ACTION_STOP -> { + player?.stop() + stopForeground(true) + stopSelf() + } + ACTION_SET_URI -> { + val uri = intent.getStringExtra(EXTRA_URI) + if (uri != null) setMediaUri(uri) + } + } + } + startForegroundIfNeeded() + return START_STICKY + } + + private fun setMediaUri(uri: String) { + initializePlayer() + val mediaItem = MediaItem.fromUri(uri) + player?.setMediaItem(mediaItem) + player?.prepare() + player?.play() + val metadataBuilder = MediaMetadataCompat.Builder() + .putString(MediaMetadataCompat.METADATA_KEY_TITLE, uri) + mediaSession.setMetadata(metadataBuilder.build()) + updatePlaybackState() + } + + override fun onDestroy() { + player?.release() + player = null + mediaSession.release() + super.onDestroy() + } + + override fun onBind(intent: Intent): IBinder? { + super.onBind(intent) + return null + } + + private fun createChannel() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val nm = getSystemService(NotificationManager::class.java) + val channel = NotificationChannel(CHANNEL_ID, "External Player", NotificationManager.IMPORTANCE_LOW) + nm.createNotificationChannel(channel) + } + } +} \ No newline at end of file From df9d654de04257932e9d1f936e513c6da8a79dca Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:07:00 +0530 Subject: [PATCH 05/20] Add MediaNotificationManager for notification handling --- .../MediaNotificationManager.kt | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 external-player/src/main/java/org/newpipe/externalplayer/MediaNotificationManager.kt diff --git a/external-player/src/main/java/org/newpipe/externalplayer/MediaNotificationManager.kt b/external-player/src/main/java/org/newpipe/externalplayer/MediaNotificationManager.kt new file mode 100644 index 00000000000..7dac102f577 --- /dev/null +++ b/external-player/src/main/java/org/newpipe/externalplayer/MediaNotificationManager.kt @@ -0,0 +1,46 @@ +package org.newpipe.externalplayer + +import android.app.Notification +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.graphics.BitmapFactory +import androidx.core.app.NotificationCompat +import androidx.media.app.NotificationCompat.MediaStyle + +class MediaNotificationManager(private val context: Context) { + + fun buildNotification(player: com.google.android.exoplayer2.ExoPlayer?, token: android.os.Parcelable?): Notification { + val playIntent = Intent(context, PlayerService::class.java).apply { action = PlayerService.ACTION_PLAY } + val pauseIntent = Intent(context, PlayerService::class.java).apply { action = PlayerService.ACTION_PAUSE } + val stopIntent = Intent(context, PlayerService::class.java).apply { action = PlayerService.ACTION_STOP } + + val playPending = PendingIntent.getService(context, 0, playIntent, PendingIntent.FLAG_IMMUTABLE) + val pausePending = PendingIntent.getService(context, 1, pauseIntent, PendingIntent.FLAG_IMMUTABLE) + val stopPending = PendingIntent.getService(context, 2, stopIntent, PendingIntent.FLAG_IMMUTABLE) + + val isPlaying = player?.isPlaying == true + val action = if (isPlaying) { + NotificationCompat.Action(android.R.drawable.ic_media_pause, "Pause", pausePending) + } else { + NotificationCompat.Action(android.R.drawable.ic_media_play, "Play", playPending) + } + + val mediaStyle = MediaStyle() + token?.let { mediaStyle.setMediaSession(it as android.media.session.MediaSession.Token) } + + val builder = NotificationCompat.Builder(context, PlayerService.CHANNEL_ID) + .setContentTitle("NewPipe External Player") + .setContentText("Playing") + .setSmallIcon(android.R.drawable.ic_media_play) + .setLargeIcon(BitmapFactory.decodeResource(context.resources, android.R.mipmap.sym_def_app_icon)) + .addAction(action) + .addAction(NotificationCompat.Action(android.R.drawable.ic_media_previous, "Stop", stopPending)) + .setStyle(mediaStyle) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setOnlyAlertOnce(true) + .setOngoing(isPlaying) + + return builder.build() + } +} \ No newline at end of file From 80ab513a271518957c7ac1029085c91e4565538d Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:07:28 +0530 Subject: [PATCH 06/20] Update ExternalPlayerActivity with service integration and UI controls --- .../externalplayer/ExternalPlayerActivity.kt | 107 +++++++----------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt b/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt index 0657ac1d3bf..8be1f16a8d7 100644 --- a/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt +++ b/external-player/src/main/java/org/newpipe/externalplayer/ExternalPlayerActivity.kt @@ -6,19 +6,18 @@ import android.net.Uri import android.os.Build import android.os.Bundle import android.util.Rational -import android.view.View +import android.view.GestureDetector +import android.view.MotionEvent import androidx.appcompat.app.AppCompatActivity -import com.google.android.exoplayer2.MediaItem -import com.google.android.exoplayer2.Player.STATE_READY -import com.google.android.exoplayer2.SimpleExoPlayer +import androidx.core.content.ContextCompat import org.newpipe.externalplayer.databinding.ActivityExternalPlayerBinding class ExternalPlayerActivity : AppCompatActivity() { private lateinit var binding: ActivityExternalPlayerBinding - private var player: SimpleExoPlayer? = null - private var playWhenReady = true - private var playbackPosition: Long = 0 + private var serviceStarted = false + private val speeds = floatArrayOf(1.0f, 1.25f, 1.5f, 2.0f, 0.5f) + private var speedIndex = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -27,8 +26,31 @@ class ExternalPlayerActivity : AppCompatActivity() { handleIncomingIntent(intent) - binding.enterPipButton.setOnClickListener { - enterPip() + binding.enterPipButton.setOnClickListener { enterPip() } + binding.speedButton.setOnClickListener { + speedIndex = (speedIndex + 1) % speeds.size + val speed = speeds[speedIndex] + binding.speedButton.text = "${speed}x" + val intent = Intent(this, PlayerService::class.java).apply { + action = PlayerService.ACTION_PLAY + putExtra("speed", speed) + } + ContextCompat.startForegroundService(this, intent) + } + binding.subToggle.setOnClickListener { + val newText = if (binding.subToggle.text == "SUB") "SUB:OFF" else "SUB" + binding.subToggle.text = newText + } + + val gestureDetector = GestureDetector(this, object : GestureDetector.SimpleOnGestureListener() { + override fun onScroll(e1: MotionEvent?, e2: MotionEvent?, distanceX: Float, distanceY: Float): Boolean { + return super.onScroll(e1, e2, distanceX, distanceY) + } + }) + + binding.playerView.setOnTouchListener { _, event -> + gestureDetector.onTouchEvent(event) + false } } @@ -39,37 +61,29 @@ class ExternalPlayerActivity : AppCompatActivity() { if (Intent.ACTION_VIEW == action || data != null) { data?.let { uri -> binding.urlText.text = uri.toString() - initializePlayer(uri.toString()) + startServiceWithUri(uri.toString()) } } else if (Intent.ACTION_SEND == action) { val text = intent.getStringExtra(Intent.EXTRA_TEXT) ?: intent.getStringExtra(Intent.EXTRA_STREAM)?.toString() text?.let { val url = extractUrlFromText(it) - if (url != null) initializePlayer(url) + if (url != null) startServiceWithUri(url) } } } - private fun extractUrlFromText(text: String): String? { - val regex = "(https?://[\w\-._~:/?#[\]@!$&'()*+,;=%]+)".toRegex() - return regex.find(text)?.value + private fun startServiceWithUri(uri: String) { + val intent = Intent(this, PlayerService::class.java).apply { + action = PlayerService.ACTION_SET_URI + putExtra(PlayerService.EXTRA_URI, uri) + } + ContextCompat.startForegroundService(this, intent) + serviceStarted = true } - private fun initializePlayer(url: String) { - if (player == null) { - player = SimpleExoPlayer.Builder(this).build() - binding.playerView.player = player - } - val mediaItem = MediaItem.fromUri(Uri.parse(url)) - player!!.setMediaItem(mediaItem) - player!!.playWhenReady = playWhenReady - player!!.seekTo(playbackPosition) - player!!.prepare() - player!!.addListener(object : com.google.android.exoplayer2.Player.Listener { - override fun onPlaybackStateChanged(state: Int) { - binding.loadingView.visibility = if (state == STATE_READY) View.GONE else View.VISIBLE - } - }) + private fun extractUrlFromText(text: String): String? { + val regex = "(https?://[\\w\\-._~:/?#[\\]@!$&'()*+,;=%]+)".toRegex() + return regex.find(text)?.value } private fun enterPip() { @@ -87,39 +101,4 @@ class ExternalPlayerActivity : AppCompatActivity() { enterPip() } } - - override fun onPause() { - super.onPause() - player?.let { - playbackPosition = it.currentPosition - playWhenReady = it.playWhenReady - it.playWhenReady = false - } - } - - override fun onResume() { - super.onResume() - player?.playWhenReady = playWhenReady - } - - override fun onStop() { - super.onStop() - if (!isInPictureInPictureMode) { - releasePlayer() - } - } - - override fun onDestroy() { - super.onDestroy() - releasePlayer() - } - - private fun releasePlayer() { - player?.let { - playbackPosition = it.currentPosition - playWhenReady = it.playWhenReady - it.release() - player = null - } - } } \ No newline at end of file From 8310569fa413c5a00ab103de755cd4083488c67c Mon Sep 17 00:00:00 2001 From: osphvdhwj <150425690+osphvdhwj@users.noreply.github.com> Date: Sat, 1 Nov 2025 18:07:54 +0530 Subject: [PATCH 07/20] Update activity layout with new UI controls --- .../res/layout/activity_external_player.xml | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/external-player/src/main/res/layout/activity_external_player.xml b/external-player/src/main/res/layout/activity_external_player.xml index 6c9a8f12abb..ef7370da2a7 100644 --- a/external-player/src/main/res/layout/activity_external_player.xml +++ b/external-player/src/main/res/layout/activity_external_player.xml @@ -30,12 +30,33 @@ android:visibility="gone" android:layout_gravity="center" /> + + +