Skip to content

Commit

Permalink
update players
Browse files Browse the repository at this point in the history
- [android] use media3-exoplayer
- [android] remove headless JS service
- [ios] cache artwork
  • Loading branch information
maxep committed Sep 15, 2023
1 parent 95f9d6b commit 0521647
Show file tree
Hide file tree
Showing 15 changed files with 2,083 additions and 3,438 deletions.
9 changes: 6 additions & 3 deletions android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ buildscript {
}

dependencies {
classpath "com.android.tools.build:gradle:8.1.0"
classpath "com.android.tools.build:gradle:8.1.1"
// noinspection DifferentKotlinGradleVersion
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
Expand Down Expand Up @@ -73,8 +73,11 @@ def kotlin_version = getExtOrDefault("kotlinVersion")
def rn_version = new File(['node', '--print',"JSON.parse(require('fs').readFileSync(require.resolve('react-native/package.json'), 'utf-8')).version"].execute(null, rootDir).text.trim())

dependencies {
implementation 'com.google.android.exoplayer:exoplayer:2.19.1'
implementation 'com.google.android.exoplayer:extension-mediasession:2.19.1'
implementation 'androidx.media3:media3-exoplayer:1.2.0-alpha01'
implementation 'androidx.media3:media3-session:1.2.0-alpha01'
implementation 'androidx.media3:media3-exoplayer-dash:1.2.0-alpha01'
implementation 'androidx.media3:media3-exoplayer-hls:1.2.0-alpha01'

// For < 0.71, this will be from the local maven repo
// For > 0.71, this will be replaced by `com.facebook.react:react-android:$version` by react gradle plugin
//noinspection GradleDynamicVersion
Expand Down
2 changes: 1 addition & 1 deletion android/gradle.properties
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Audio_kotlinVersion=1.7.22
Audio_minSdkVersion=21
Audio_targetSdkVersion=33
Audio_compileSdkVersion=33
Audio_compileSdkVersion=34
Audio_ndkversion=23.1.7779620

# AndroidX package structure to make it clearer which packages are bundled with the
Expand Down
12 changes: 10 additions & 2 deletions android/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,10 +1,18 @@
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
package="lyl.reactnativeaudio">

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />

<application>
<service android:name=".AudioService" android:enabled="true" android:exported="false"/>
<service
android:name=".AudioService"
android:foregroundServiceType="mediaPlayback"
android:exported="true">
<intent-filter>
<action android:name="androidx.media3.session.MediaSessionService"/>
</intent-filter>
</service>
</application>
</manifest>
201 changes: 129 additions & 72 deletions android/src/main/java/lyl/reactnativeaudio/AudioModule.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,22 @@
package lyl.reactnativeaudio

import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.net.Uri
import android.os.IBinder
import android.os.Handler
import android.util.Log
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.Timeline
import androidx.media3.session.MediaController
import androidx.media3.session.SessionToken
import com.facebook.react.bridge.*
import com.facebook.react.modules.core.DeviceEventManagerModule
import com.google.common.util.concurrent.ListenableFuture
import com.google.common.util.concurrent.MoreExecutors
import kotlin.math.max

class AudioModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {

Expand All @@ -25,11 +35,27 @@ class AudioModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
}

private val context = reactContext
private var service: AudioService? = null

override fun getName(): String { return "Audio" }

override fun hasConstants(): Boolean { return true }
private lateinit var controllerFuture: ListenableFuture<MediaController>
private val controller: MediaController?
get() = if (controllerFuture.isDone) controllerFuture.get() else null
private var progressHandler: Handler? = null

override fun getName() = "Audio"
override fun hasConstants() = true

override fun initialize() {
super.initialize()

controllerFuture =
MediaController.Builder(
context,
SessionToken(context, ComponentName(context, AudioService::class.java))
).buildAsync()

controllerFuture.addListener({
controller?.addListener(playerListener)
}, MoreExecutors.directExecutor())
}

override fun getConstants(): MutableMap<String, Any> {
return mutableMapOf(
Expand All @@ -45,103 +71,134 @@ class AudioModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaM
)
}

// Module CMD
private fun sendEvent(eventName: String, params: Any?) {
context
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
?.emit(eventName, params)
}

@ReactMethod
fun play(data: ReadableMap, promise: Promise) {
try {
val string = data.getString("uri") ?: throw IllegalArgumentException("uri field required")
val uri = Uri.parse(string)
private fun startProgressListener() {
if (progressHandler != null) return
val controller = controller ?: return
progressHandler = Handler(controller.applicationLooper)
progressHandler?.post(onPlayerProgressChanged)
}

private fun stopProgressListener() {
progressHandler?.removeCallbacks(onPlayerProgressChanged)
progressHandler = null
}

if (uri == service?.audio?.uri) return resume(promise)
private fun metadata(data: ReadableMap): MediaMetadata {
return MediaMetadata.Builder()
.setTitle(data.getString("title"))
.setAlbumTitle(data.getString("album"))
.setArtist(data.getString("artist"))
.setAlbumArtist(data.getString("albumArtist"))
.setArtworkUri(
data.getString("artwork").let { Uri.parse(it) }
)
.build()
}

val audio = AudioService.Audio(uri, data.toHashMap())
bindAudioService(audio)
// Module CMD

@ReactMethod
fun source(data: ReadableMap, promise: Promise) {
try {
val controller = controller ?: throw IllegalArgumentException("Audio module not initialized")
val uri = data.getString("uri") ?: throw IllegalArgumentException("uri field required")

// Build the media item.
val mediaItem = MediaItem.Builder()
.setUri(uri)
.setMediaId(uri)
.setMediaMetadata(metadata(data))
.build()

if (uri == controller.currentMediaItem?.mediaId) {
controller.replaceMediaItem(0, mediaItem)
return promise.resolve(null)
}

// Set the media item to be played.
controller.setMediaItem(mediaItem)
promise.resolve(null)
} catch (e: Exception) {
promise.reject(e)
}
}

@ReactMethod
fun update(data: ReadableMap, promise: Promise) {
context.runOnUiQueueThread {
service?.prepareNotification(context, data.toHashMap())
promise.resolve(null)
}
fun play(promise: Promise) {
controller?.prepare()
controller?.play()
promise.resolve(null)
}

@ReactMethod
fun seekTo(position: Double, promise: Promise) {
context.runOnUiQueueThread {
service?.seekTo(position)
promise.resolve(null)
}
val positionMs = position.toLong() * 1000
controller?.seekTo(positionMs)
promise.resolve(null)
}

@ReactMethod
fun pause(promise: Promise) {
context.runOnUiQueueThread {
service?.playWhenReady(false)
promise.resolve(null)
}
}

@ReactMethod
fun resume(promise: Promise) {
context.runOnUiQueueThread {
service?.playWhenReady(true)
promise.resolve(null)
}
controller?.pause()
promise.resolve(null)
}

@ReactMethod
fun stop(promise: Promise) {
context.runOnUiQueueThread {
service?.stop()
unbindAudioService()
promise.resolve(null)
}
controller?.stop()
controller?.seekTo(0)
promise.resolve(null)
}

// Audio Service Binding

private val connection = object : ServiceConnection {

var audio: AudioService.Audio? = null

override fun onServiceConnected(name: ComponentName?, binder: IBinder?) {
val binder = binder as AudioService.AudioServiceBinder
service = binder.service

val audio = audio ?: return
prepareAudioService(audio)
// Player.Listener

private val playerListener = object : Player.Listener {
override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying) {
startProgressListener()
sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_PLAYING)
} else {
sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_PAUSED)
stopProgressListener()
}
}

override fun onServiceDisconnected(name: ComponentName?) {
service = null
override fun onPlaybackStateChanged(playbackState: Int) {
when (playbackState) {
Player.STATE_BUFFERING -> sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_BUFFERING)
Player.STATE_ENDED -> sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_ENDED)
Player.STATE_IDLE -> { sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_PAUSED) }
Player.STATE_READY -> { }
}
}
}

private fun bindAudioService(audio: AudioService.Audio) {
if (service != null) return prepareAudioService(audio)
override fun onTimelineChanged(timeline: Timeline, reason: Int) {
val controller = controller ?: return
if (controller.duration == C.TIME_UNSET) return
val duration = controller.duration / 1000
sendEvent(PLAYER_DURATION_EVENT, duration.toDouble())
}

connection.audio = audio
Intent(context, AudioService::class.java).also { intent ->
context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
override fun onPlayerError(error: PlaybackException) {
sendEvent(PLAYER_STATE_EVENT, PLAYER_STATE_UNKNOWN)
}
}

private fun unbindAudioService() {
if (service == null) return
context.unbindService(connection)
service = null
}
// Progress Listener

private val onPlayerProgressChanged = object : Runnable {
override fun run() {
val controller = controller ?: return
progressHandler?.postDelayed(this, 200)

private fun prepareAudioService(audio: AudioService.Audio) {
context.runOnUiQueueThread {
service?.preparePlayer(context, audio)
val position = max(0, controller.currentPosition / 1000)
sendEvent(PLAYER_PROGRESS_EVENT, position.toDouble())
}
}
}
4 changes: 0 additions & 4 deletions android/src/main/java/lyl/reactnativeaudio/AudioPackage.kt
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
package lyl.reactnativeaudio

import java.util.Arrays
import java.util.Collections

import com.facebook.react.ReactPackage
import com.facebook.react.bridge.NativeModule
import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.uimanager.ViewManager
import com.facebook.react.bridge.JavaScriptModule

class AudioPackage : ReactPackage {
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> {
Expand Down
Loading

0 comments on commit 0521647

Please sign in to comment.