mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59218e8fa0 | ||
|
|
f98c787b15 |
@@ -151,7 +151,7 @@ dependencies {
|
|||||||
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
|
implementation("androidx.media3:media3-exoplayer-hls:1.10.0")
|
||||||
|
|
||||||
implementation("androidx.media3:media3-cast:1.10.0")
|
implementation("androidx.media3:media3-cast:1.10.0")
|
||||||
implementation("com.google.android.gms:play-services-cast-framework:22.3.1")
|
implementation("com.google.android.gms:play-services-cast-framework:21.5.0")
|
||||||
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ class OmniCastOptionsProvider : OptionsProvider {
|
|||||||
|
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder()
|
||||||
.setReceiverApplicationId(appId)
|
.setReceiverApplicationId(appId)
|
||||||
.setShowSystemOutputSwitcherOnCastIconClick(true)
|
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class OmniCastTrackSelector : CastTrackSelector() {
|
|||||||
indices.forEach { selections.remove(groups[it]) }
|
indices.forEach { selections.remove(groups[it]) }
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
|
|
||||||
|
// An explicit override selects an exact track by identity; this wins over
|
||||||
|
// language/label matching, which can't disambiguate same-language tracks.
|
||||||
val overrides = request.trackSelectionParameters.overrides
|
val overrides = request.trackSelectionParameters.overrides
|
||||||
val overrideMatch = indices.firstOrNull { i ->
|
val overrideMatch = indices.firstOrNull { i ->
|
||||||
overrides.keys.any { it.id == groups[i].id }
|
overrides.keys.any { it.id == groups[i].id }
|
||||||
|
|||||||
@@ -43,14 +43,8 @@ class OmniMediaItemConverter : MediaItemConverter {
|
|||||||
val meta = mediaItem.mediaMetadata
|
val meta = mediaItem.mediaMetadata
|
||||||
val castMetadata = CastMediaMetadata(CastMediaMetadata.MEDIA_TYPE_MOVIE).apply {
|
val castMetadata = CastMediaMetadata(CastMediaMetadata.MEDIA_TYPE_MOVIE).apply {
|
||||||
meta.title?.let { putString(CastMediaMetadata.KEY_TITLE, it.toString()) }
|
meta.title?.let { putString(CastMediaMetadata.KEY_TITLE, it.toString()) }
|
||||||
meta.artist?.let {
|
meta.artist?.let { putString(CastMediaMetadata.KEY_ARTIST, it.toString()) }
|
||||||
putString(CastMediaMetadata.KEY_ARTIST, it.toString())
|
meta.albumTitle?.let { putString(CastMediaMetadata.KEY_ALBUM_TITLE, it.toString()) }
|
||||||
putString(CastMediaMetadata.KEY_SUBTITLE, it.toString())
|
|
||||||
}
|
|
||||||
meta.albumTitle?.let {
|
|
||||||
putString(CastMediaMetadata.KEY_ALBUM_TITLE, it.toString())
|
|
||||||
putString(CastMediaMetadata.KEY_STUDIO, it.toString())
|
|
||||||
}
|
|
||||||
meta.artworkUri?.let { addImage(WebImage(it)) }
|
meta.artworkUri?.let { addImage(WebImage(it)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +57,7 @@ class OmniMediaItemConverter : MediaItemConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tracks = local.subtitleConfigurations.mapIndexed { index, subtitle ->
|
val tracks = local.subtitleConfigurations.mapIndexed { index, subtitle ->
|
||||||
// caf tracks are 1 based instead of 0 based. videojs does the same
|
MediaTrack.Builder(index.toLong(), MediaTrack.TYPE_TEXT)
|
||||||
MediaTrack.Builder((index + 1).toLong(), MediaTrack.TYPE_TEXT)
|
|
||||||
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
|
.setSubtype(MediaTrack.SUBTYPE_SUBTITLES)
|
||||||
.setContentId(subtitle.uri.toString())
|
.setContentId(subtitle.uri.toString())
|
||||||
.setContentType(subtitle.mimeType ?: "text/vtt")
|
.setContentType(subtitle.mimeType ?: "text/vtt")
|
||||||
|
|||||||
@@ -37,7 +37,9 @@ import androidx.media3.session.DefaultMediaNotificationProvider
|
|||||||
import androidx.media3.session.MediaSession
|
import androidx.media3.session.MediaSession
|
||||||
import androidx.media3.session.MediaSessionService
|
import androidx.media3.session.MediaSessionService
|
||||||
import androidx.mediarouter.app.MediaRouteChooserDialog
|
import androidx.mediarouter.app.MediaRouteChooserDialog
|
||||||
import androidx.mediarouter.app.SystemOutputSwitcherDialogController
|
import androidx.mediarouter.media.MediaRouteSelector
|
||||||
|
import androidx.mediarouter.media.MediaRouter
|
||||||
|
import com.google.android.gms.cast.CastMediaControlIntent
|
||||||
import com.google.android.gms.cast.framework.CastContext
|
import com.google.android.gms.cast.framework.CastContext
|
||||||
import com.google.android.gms.cast.framework.CastState
|
import com.google.android.gms.cast.framework.CastState
|
||||||
import com.google.android.gms.cast.framework.CastStateListener
|
import com.google.android.gms.cast.framework.CastStateListener
|
||||||
@@ -78,6 +80,14 @@ class OmniPlayer(
|
|||||||
private val castStateListener =
|
private val castStateListener =
|
||||||
CastStateListener { eventMap.emitCastStatus(computeCastStatus()) }
|
CastStateListener { eventMap.emitCastStatus(computeCastStatus()) }
|
||||||
|
|
||||||
|
// Without a MediaRouteButton the Cast SDK never actively scans, so
|
||||||
|
// CastContext.castState stays NO_DEVICES_AVAILABLE (reported as
|
||||||
|
// "unavailable") even when receivers are on the network. Hold an active
|
||||||
|
// discovery request so castState reflects real availability. MediaRouter
|
||||||
|
// must only be touched on the main thread.
|
||||||
|
private var mediaRouter: MediaRouter? = null
|
||||||
|
private val mediaRouterCallback = object : MediaRouter.Callback() {}
|
||||||
|
|
||||||
val player: Player = runOnMainThreadSync {
|
val player: Player = runOnMainThreadSync {
|
||||||
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
|
castOptions?.receiverApplicationId?.let { receiverApplicationId = it }
|
||||||
val cc = try {
|
val cc = try {
|
||||||
@@ -91,6 +101,21 @@ class OmniPlayer(
|
|||||||
localPlayer
|
localPlayer
|
||||||
} else {
|
} else {
|
||||||
cc.addCastStateListener(castStateListener)
|
cc.addCastStateListener(castStateListener)
|
||||||
|
// Build the discovery selector from the receiver app id directly
|
||||||
|
// (cc.mergedSelector can be null this early), then keep an active
|
||||||
|
// discovery request so castState reflects real availability.
|
||||||
|
val appId = receiverApplicationId
|
||||||
|
?: CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID
|
||||||
|
val selector = cc.mergedSelector ?: MediaRouteSelector.Builder()
|
||||||
|
.addControlCategory(CastMediaControlIntent.categoryForCast(appId))
|
||||||
|
.build()
|
||||||
|
val router = MediaRouter.getInstance(ctx)
|
||||||
|
router.addCallback(
|
||||||
|
selector,
|
||||||
|
mediaRouterCallback,
|
||||||
|
MediaRouter.CALLBACK_FLAG_REQUEST_DISCOVERY,
|
||||||
|
)
|
||||||
|
mediaRouter = router
|
||||||
val remote = RemoteCastPlayer.Builder(ctx)
|
val remote = RemoteCastPlayer.Builder(ctx)
|
||||||
.setMediaItemConverter(OmniMediaItemConverter())
|
.setMediaItemConverter(OmniMediaItemConverter())
|
||||||
.setTrackSelector(OmniCastTrackSelector())
|
.setTrackSelector(OmniCastTrackSelector())
|
||||||
@@ -106,34 +131,21 @@ class OmniPlayer(
|
|||||||
|
|
||||||
override var showNotification: Boolean? = false
|
override var showNotification: Boolean? = false
|
||||||
set(value) {
|
set(value) {
|
||||||
field = value
|
if (value == true) {
|
||||||
syncNotificationService()
|
|
||||||
}
|
|
||||||
|
|
||||||
private var serviceRunning = false
|
|
||||||
|
|
||||||
private fun syncNotificationService() {
|
|
||||||
val shouldShow = showNotification == true && source != null
|
|
||||||
when {
|
|
||||||
shouldShow && !serviceRunning -> {
|
|
||||||
val otherIsPlaying = notificationPlayer?.let { other ->
|
val otherIsPlaying = notificationPlayer?.let { other ->
|
||||||
other !== localPlayer && runOnMainThreadSync { other.isPlaying }
|
runOnMainThreadSync { other.isPlaying }
|
||||||
} == true
|
} == true
|
||||||
if (otherIsPlaying) {
|
if (otherIsPlaying) {
|
||||||
throw Error("Two players can't display notifications at the same time.")
|
throw Error("Two players can't display notifications at the same time.")
|
||||||
}
|
}
|
||||||
notificationPlayer = localPlayer
|
notificationPlayer = localPlayer
|
||||||
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
ctx.startForegroundService(Intent(ctx, OmniPlayerService::class.java))
|
||||||
serviceRunning = true
|
} else if (field == true && notificationPlayer == localPlayer) {
|
||||||
}
|
|
||||||
|
|
||||||
!shouldShow && serviceRunning -> {
|
|
||||||
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
ctx.stopService(Intent(ctx, OmniPlayerService::class.java))
|
||||||
if (notificationPlayer == localPlayer) notificationPlayer = null
|
notificationPlayer = null
|
||||||
serviceRunning = false
|
|
||||||
}
|
}
|
||||||
|
field = value
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
override val castStatus: CastStatus
|
override val castStatus: CastStatus
|
||||||
get() = runOnMainThreadSync { computeCastStatus() }
|
get() = runOnMainThreadSync { computeCastStatus() }
|
||||||
@@ -161,9 +173,6 @@ class OmniPlayer(
|
|||||||
return@runOnMainThread
|
return@runOnMainThread
|
||||||
}
|
}
|
||||||
val activity = ctx.currentActivity ?: return@runOnMainThread
|
val activity = ctx.currentActivity ?: return@runOnMainThread
|
||||||
if (SystemOutputSwitcherDialogController.showDialog(activity)) {
|
|
||||||
return@runOnMainThread
|
|
||||||
}
|
|
||||||
val selector = cc.mergedSelector ?: return@runOnMainThread
|
val selector = cc.mergedSelector ?: return@runOnMainThread
|
||||||
MediaRouteChooserDialog(activity).apply { routeSelector = selector }.show()
|
MediaRouteChooserDialog(activity).apply { routeSelector = selector }.show()
|
||||||
}
|
}
|
||||||
@@ -176,6 +185,7 @@ class OmniPlayer(
|
|||||||
eventMap.dispose()
|
eventMap.dispose()
|
||||||
runOnMainThread {
|
runOnMainThread {
|
||||||
castContext?.removeCastStateListener(castStateListener)
|
castContext?.removeCastStateListener(castStateListener)
|
||||||
|
mediaRouter?.removeCallback(mediaRouterCallback)
|
||||||
// release both cast and local players.
|
// release both cast and local players.
|
||||||
player.release()
|
player.release()
|
||||||
}
|
}
|
||||||
@@ -308,9 +318,9 @@ class OmniPlayer(
|
|||||||
field = value
|
field = value
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
runOnMainThreadSync {
|
runOnMainThreadSync {
|
||||||
player.clearMediaItems()
|
player.setMediaItem(MediaItem.EMPTY)
|
||||||
|
player.prepare()
|
||||||
}
|
}
|
||||||
syncNotificationService()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val handleAudioFocus =
|
val handleAudioFocus =
|
||||||
@@ -343,7 +353,6 @@ class OmniPlayer(
|
|||||||
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
|
||||||
player.prepare()
|
player.prepare()
|
||||||
}
|
}
|
||||||
syncNotificationService()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun play() {
|
override fun play() {
|
||||||
@@ -381,8 +390,10 @@ class OmniPlayer(
|
|||||||
|
|
||||||
override fun selectAudio(audio: Track) = selectTrack(C.TRACK_TYPE_AUDIO, audio)
|
override fun selectAudio(audio: Track) = selectTrack(C.TRACK_TYPE_AUDIO, audio)
|
||||||
|
|
||||||
override fun selectSubtitle(subtitle: Track?) = selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
// Select the exact track by its group id (stable identity) rather than by
|
||||||
|
// preferred language/label: two same-language tracks, or tracks whose labels
|
||||||
|
// don't survive the cast round-trip, can't be told apart by preference. Falls
|
||||||
|
// back to language/label only when the group can't be found yet.
|
||||||
private fun selectTrack(type: Int, track: Track?) {
|
private fun selectTrack(type: Int, track: Track?) {
|
||||||
runOnMainThreadSync {
|
runOnMainThreadSync {
|
||||||
val params = player.trackSelectionParameters.buildUpon()
|
val params = player.trackSelectionParameters.buildUpon()
|
||||||
@@ -411,34 +422,28 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ASS/PGS subtitles are declared as regular cast tracks; the receiver draws
|
||||||
|
// the ones it can't render natively, keyed off the active track id. Selection
|
||||||
|
// is uniform (see selectTrack): pick the exact track, which maps to
|
||||||
|
// activeTrackIds while casting and to native tracks locally.
|
||||||
|
override fun selectSubtitle(subtitle: Track?) = selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
||||||
|
|
||||||
private fun tracksByType(trackType: Int): Array<Track> {
|
private fun tracksByType(trackType: Int): Array<Track> {
|
||||||
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
val groups = player.currentTracks.groups.filter { it.type == trackType }
|
||||||
if (groups.isEmpty()) return emptyArray()
|
if (groups.isEmpty()) return emptyArray()
|
||||||
|
|
||||||
// when casting tracks are stripped of some metadata, recover it
|
return groups.map {
|
||||||
val castTracks = castMediaTracksById()
|
it.getTrackFormat(0).run {
|
||||||
|
Track(
|
||||||
return groups.map { group ->
|
id = it.mediaTrackGroup.id,
|
||||||
val format = group.getTrackFormat(0)
|
label = this.label,
|
||||||
|
language = this.language,
|
||||||
val castTrackId = group.mediaTrackGroup.id.substringAfterLast("track=", "").toLongOrNull()
|
selected = it.isSelected
|
||||||
val cast = castTracks[castTrackId]
|
)
|
||||||
Track(
|
}
|
||||||
id = group.mediaTrackGroup.id,
|
|
||||||
label = cast?.name ?: format.label,
|
|
||||||
language = cast?.language ?: format.language,
|
|
||||||
selected = group.isSelected
|
|
||||||
)
|
|
||||||
}.toTypedArray()
|
}.toTypedArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun castMediaTracksById(): Map<Long, com.google.android.gms.cast.MediaTrack> {
|
|
||||||
if (!isCasting) return emptyMap()
|
|
||||||
val tracks = castContext?.sessionManager?.currentCastSession
|
|
||||||
?.remoteMediaClient?.mediaInfo?.mediaTracks ?: return emptyMap()
|
|
||||||
return tracks.associateBy { it.id }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRenditions(): Array<Rendition> {
|
private fun getRenditions(): Array<Rendition> {
|
||||||
val group =
|
val group =
|
||||||
player.currentTracks.groups.firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
|
player.currentTracks.groups.firstOrNull { it.isSelected && it.type == C.TRACK_TYPE_VIDEO }
|
||||||
@@ -519,13 +524,7 @@ class OmniPlayerService : MediaSessionService() {
|
|||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
val available = OmniPlayer.notificationPlayer
|
player = OmniPlayer.notificationPlayer ?: throw Error("No player available")
|
||||||
if (available == null) {
|
|
||||||
startForeground(1, createImmediateNotification())
|
|
||||||
stopSelf()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
player = available
|
|
||||||
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
val launchIntent = packageManager.getLaunchIntentForPackage(packageName)?.apply {
|
||||||
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
|
||||||
}
|
}
|
||||||
@@ -561,8 +560,7 @@ class OmniPlayerService : MediaSessionService() {
|
|||||||
player = current
|
player = current
|
||||||
mediaSession.player = current
|
mediaSession.player = current
|
||||||
}
|
}
|
||||||
super.onStartCommand(intent, flags, startId)
|
return super.onStartCommand(intent, flags, startId)
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createImmediateNotification(): Notification {
|
private fun createImmediateNotification(): Notification {
|
||||||
@@ -603,7 +601,7 @@ class OmniPlayerService : MediaSessionService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
if (::mediaSession.isInitialized) mediaSession.release()
|
mediaSession.release()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -689,27 +689,25 @@ class VlcPlayer(ctx: Context) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentTimeline(): Timeline {
|
override fun getCurrentTimeline(): Timeline {
|
||||||
// snapshot the current playlist for the returned timeline.
|
if (mediaItems.isEmpty()) return Timeline.EMPTY
|
||||||
// timelines are immutable
|
|
||||||
val items = mediaItems
|
|
||||||
val currentIndex = currentMediaItemIndex
|
|
||||||
val currentDurationUs = duration.takeIf { it != TIME_UNSET }?.let { it * 1000L } ?: TIME_UNSET
|
|
||||||
if (items.isEmpty()) return Timeline.EMPTY
|
|
||||||
return object : Timeline() {
|
return object : Timeline() {
|
||||||
private fun durationUsForIndex(index: Int): Long =
|
private fun durationUsForIndex(index: Int): Long {
|
||||||
if (index == currentIndex) currentDurationUs else TIME_UNSET
|
if (index != currentMediaItemIndex) return TIME_UNSET
|
||||||
|
val dur = duration
|
||||||
|
return if (dur == TIME_UNSET) TIME_UNSET else dur * 1000L
|
||||||
|
}
|
||||||
|
|
||||||
override fun getWindowCount(): Int = items.size
|
override fun getWindowCount(): Int = mediaItems.size
|
||||||
|
|
||||||
override fun getWindow(
|
override fun getWindow(
|
||||||
windowIndex: Int,
|
windowIndex: Int,
|
||||||
window: Window,
|
window: Window,
|
||||||
defaultPositionProjectionUs: Long
|
defaultPositionProjectionUs: Long
|
||||||
): Window {
|
): Window {
|
||||||
val index = windowIndex.coerceIn(items.indices)
|
check(windowIndex in mediaItems.indices)
|
||||||
return window.set(
|
return window.set(
|
||||||
index,
|
windowIndex,
|
||||||
items[index],
|
mediaItems[windowIndex],
|
||||||
null,
|
null,
|
||||||
TIME_UNSET,
|
TIME_UNSET,
|
||||||
TIME_UNSET,
|
TIME_UNSET,
|
||||||
@@ -718,33 +716,34 @@ class VlcPlayer(ctx: Context) :
|
|||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
0L,
|
0L,
|
||||||
durationUsForIndex(index),
|
durationUsForIndex(windowIndex),
|
||||||
index,
|
0,
|
||||||
index,
|
mediaItems.size - 1,
|
||||||
0L
|
0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getPeriodCount(): Int = items.size
|
override fun getPeriodCount(): Int = mediaItems.size
|
||||||
|
|
||||||
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
override fun getPeriod(periodIndex: Int, period: Period, setIds: Boolean): Period {
|
||||||
val index = periodIndex.coerceIn(items.indices)
|
check(periodIndex in mediaItems.indices)
|
||||||
return period.set(index, index, index, durationUsForIndex(index), 0L)
|
return period.set(periodIndex, periodIndex, 0, durationUsForIndex(periodIndex), 0L)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getIndexOfPeriod(uid: Any): Int {
|
override fun getIndexOfPeriod(uid: Any): Int {
|
||||||
return if (uid is Int && uid in items.indices) uid else INDEX_UNSET
|
return if (uid is Int && uid in mediaItems.indices) uid else INDEX_UNSET
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getUidOfPeriod(periodIndex: Int): Any {
|
override fun getUidOfPeriod(periodIndex: Int): Any {
|
||||||
return periodIndex.coerceIn(items.indices)
|
check(periodIndex in mediaItems.indices)
|
||||||
|
return periodIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getCurrentPeriodIndex() = currentMediaItemIndex.coerceAtLeast(0)
|
override fun getCurrentPeriodIndex() = currentMediaItemIndex
|
||||||
|
|
||||||
override fun getCurrentMediaItemIndex() = currentMediaItemIndex.coerceAtLeast(0)
|
override fun getCurrentMediaItemIndex() = currentMediaItemIndex
|
||||||
|
|
||||||
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
|
override fun getDuration(): Long = player.length.takeIf { it > 0 } ?: TIME_UNSET
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
module.exports = require("./plugin/build").default;
|
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export {
|
||||||
|
default,
|
||||||
|
withCast,
|
||||||
|
withMediaNotifications,
|
||||||
|
withPip,
|
||||||
|
} from "./plugin/src/index";
|
||||||
+7
-33
@@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
import { Pressable, ScrollView, StyleSheet, Text, View } from "react-native";
|
||||||
import {
|
import {
|
||||||
type AndroidBackend,
|
type AndroidBackend,
|
||||||
@@ -57,16 +57,12 @@ function PlayerExample({
|
|||||||
trackLabel,
|
trackLabel,
|
||||||
backend,
|
backend,
|
||||||
onSwitchBackend,
|
onSwitchBackend,
|
||||||
hasSource,
|
|
||||||
onLoad,
|
|
||||||
}: {
|
}: {
|
||||||
onPrev: () => void;
|
onPrev: () => void;
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
trackLabel: string;
|
trackLabel: string;
|
||||||
backend: AndroidBackend;
|
backend: AndroidBackend;
|
||||||
onSwitchBackend: (backend: AndroidBackend) => void;
|
onSwitchBackend: (backend: AndroidBackend) => void;
|
||||||
hasSource: boolean;
|
|
||||||
onLoad: () => void;
|
|
||||||
}): React.JSX.Element {
|
}): React.JSX.Element {
|
||||||
const player = usePlayer();
|
const player = usePlayer();
|
||||||
const status = usePlayerState("status");
|
const status = usePlayerState("status");
|
||||||
@@ -79,8 +75,7 @@ function PlayerExample({
|
|||||||
const volume = usePlayerState("volume");
|
const volume = usePlayerState("volume");
|
||||||
const isAutoQuality = usePlayerState("isAutoQuality");
|
const isAutoQuality = usePlayerState("isAutoQuality");
|
||||||
const castStatus = usePlayerState("castStatus");
|
const castStatus = usePlayerState("castStatus");
|
||||||
const [logs, setLogs] = useState<{ id: number; message: string }[]>([]);
|
const [logs, setLogs] = useState<string[]>([]);
|
||||||
const logId = useRef(0);
|
|
||||||
const [tracks, setTracks] = useState(() => ({
|
const [tracks, setTracks] = useState(() => ({
|
||||||
videos: [...player.videos],
|
videos: [...player.videos],
|
||||||
audios: [...player.audios],
|
audios: [...player.audios],
|
||||||
@@ -103,7 +98,7 @@ function PlayerExample({
|
|||||||
|
|
||||||
const pushLog = useCallback((message: string) => {
|
const pushLog = useCallback((message: string) => {
|
||||||
setLogs((prev) => {
|
setLogs((prev) => {
|
||||||
const next = [{ id: logId.current++, message }, ...prev];
|
const next = [message, ...prev];
|
||||||
return next.slice(0, 8);
|
return next.slice(0, 8);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -237,20 +232,7 @@ function PlayerExample({
|
|||||||
return (
|
return (
|
||||||
<ScrollView style={styles.container}>
|
<ScrollView style={styles.container}>
|
||||||
<Text style={styles.heading}>react-native-omni</Text>
|
<Text style={styles.heading}>react-native-omni</Text>
|
||||||
<Text style={styles.subheading}>
|
<Text style={styles.subheading}>{trackLabel}</Text>
|
||||||
{hasSource ? trackLabel : "No media loaded"}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<View style={styles.row}>
|
|
||||||
<Pressable
|
|
||||||
style={[styles.button, hasSource && styles.selectedTrackButton]}
|
|
||||||
onPress={onLoad}
|
|
||||||
>
|
|
||||||
<Text style={styles.buttonText}>
|
|
||||||
{hasSource ? "Unload media" : "Load media"}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.row}>
|
<View style={styles.row}>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -502,8 +484,8 @@ function PlayerExample({
|
|||||||
<Text style={styles.logText}>Event log will appear here.</Text>
|
<Text style={styles.logText}>Event log will appear here.</Text>
|
||||||
) : (
|
) : (
|
||||||
logs.map((entry) => (
|
logs.map((entry) => (
|
||||||
<Text key={entry.id} style={styles.logText}>
|
<Text key={`${entry}`} style={styles.logText}>
|
||||||
{entry.message}
|
{entry}
|
||||||
</Text>
|
</Text>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -514,9 +496,6 @@ function PlayerExample({
|
|||||||
|
|
||||||
function App(): React.JSX.Element {
|
function App(): React.JSX.Element {
|
||||||
const [currentIndex, setCurrentIndex] = useState(0);
|
const [currentIndex, setCurrentIndex] = useState(0);
|
||||||
// Start with no media loaded so we can verify the app boots (and the media
|
|
||||||
// notification stays hidden) before any source is set.
|
|
||||||
const [hasSource, setHasSource] = useState(false);
|
|
||||||
const [backend, setBackend] = useState<AndroidBackend>("vlc");
|
const [backend, setBackend] = useState<AndroidBackend>("vlc");
|
||||||
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
|
const [pendingBackend, setPendingBackend] = useState<AndroidBackend | null>(
|
||||||
null,
|
null,
|
||||||
@@ -557,8 +536,6 @@ function App(): React.JSX.Element {
|
|||||||
{
|
{
|
||||||
id: "kusu",
|
id: "kusu",
|
||||||
link: "https://jassub.pages.dev/subtitles/Kusriya%20S2%20OP1v3.ass",
|
link: "https://jassub.pages.dev/subtitles/Kusriya%20S2%20OP1v3.ass",
|
||||||
label: "ass test",
|
|
||||||
language: "jp",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pgs",
|
id: "pgs",
|
||||||
@@ -594,9 +571,8 @@ function App(): React.JSX.Element {
|
|||||||
return (
|
return (
|
||||||
<OmniProvider
|
<OmniProvider
|
||||||
key={backend}
|
key={backend}
|
||||||
source={hasSource ? source : undefined}
|
source={source}
|
||||||
backend={{ android: backend }}
|
backend={{ android: backend }}
|
||||||
cast={{receiverApplicationId: "D8FB0FC1"}}
|
|
||||||
showNotification
|
showNotification
|
||||||
>
|
>
|
||||||
<PlayerExample
|
<PlayerExample
|
||||||
@@ -605,8 +581,6 @@ function App(): React.JSX.Element {
|
|||||||
trackLabel={PLAYLIST[currentIndex].title}
|
trackLabel={PLAYLIST[currentIndex].title}
|
||||||
backend={backend}
|
backend={backend}
|
||||||
onSwitchBackend={handleSwitchBackend}
|
onSwitchBackend={handleSwitchBackend}
|
||||||
hasSource={hasSource}
|
|
||||||
onLoad={() => setHasSource((v) => !v)}
|
|
||||||
/>
|
/>
|
||||||
</OmniProvider>
|
</OmniProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,8 +28,5 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
android:name="com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
|
||||||
android:value="dev.zoriya.omni.OmniCastOptionsProvider" />
|
android:value="dev.zoriya.omni.OmniCastOptionsProvider" />
|
||||||
<receiver
|
|
||||||
android:name="androidx.mediarouter.media.MediaTransferReceiver"
|
|
||||||
android:exported="true" />
|
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
+4
-8
@@ -12,9 +12,8 @@
|
|||||||
"android": " cd example && bunx react-native run-android --no-packager --port 8082",
|
"android": " cd example && bunx react-native run-android --no-packager --port 8082",
|
||||||
"web": "cd example && bun web",
|
"web": "cd example && bun web",
|
||||||
"typecheck": "tsc --noEmit && tsc -p plugin/tsconfig.json --noEmit",
|
"typecheck": "tsc --noEmit && tsc -p plugin/tsconfig.json --noEmit",
|
||||||
"build:plugin": "tsc -p plugin/tsconfig.json",
|
"build": "bun run typecheck && bob build",
|
||||||
"build": "bun run typecheck && bob build && bun run build:plugin",
|
"prepare": "bob build",
|
||||||
"prepare": "bun run build",
|
|
||||||
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js",
|
"codegen": "nitrogen --logLevel=\"debug\" && node post-script.js",
|
||||||
"release": "semantic-release"
|
"release": "semantic-release"
|
||||||
},
|
},
|
||||||
@@ -41,8 +40,8 @@
|
|||||||
"ios/**/*.mm",
|
"ios/**/*.mm",
|
||||||
"ios/**/*.cpp",
|
"ios/**/*.cpp",
|
||||||
"ios/**/*.swift",
|
"ios/**/*.swift",
|
||||||
"app.plugin.js",
|
"app.plugin.ts",
|
||||||
"plugin/build",
|
"plugin/src",
|
||||||
"*.podspec",
|
"*.podspec",
|
||||||
"README.md"
|
"README.md"
|
||||||
],
|
],
|
||||||
@@ -98,8 +97,5 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
},
|
|
||||||
"patchedDependencies": {
|
|
||||||
"jassub@2.5.7": "patches/jassub@2.5.7.patch"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import {
|
|||||||
const OPTIONS_PROVIDER_NAME =
|
const OPTIONS_PROVIDER_NAME =
|
||||||
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME";
|
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME";
|
||||||
const OPTIONS_PROVIDER_VALUE = "dev.zoriya.omni.OmniCastOptionsProvider";
|
const OPTIONS_PROVIDER_VALUE = "dev.zoriya.omni.OmniCastOptionsProvider";
|
||||||
const MEDIA_TRANSFER_RECEIVER_NAME =
|
|
||||||
"androidx.mediarouter.media.MediaTransferReceiver";
|
|
||||||
|
|
||||||
function patchManifestForCast(
|
function patchManifestForCast(
|
||||||
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
androidManifest: AndroidConfig.Manifest.AndroidManifest,
|
||||||
@@ -38,21 +36,6 @@ function patchManifestForCast(
|
|||||||
}
|
}
|
||||||
mainApplication["meta-data"] = metaData;
|
mainApplication["meta-data"] = metaData;
|
||||||
|
|
||||||
const receivers = mainApplication.receiver ?? [];
|
|
||||||
if (
|
|
||||||
!receivers.some(
|
|
||||||
(item) => item.$["android:name"] === MEDIA_TRANSFER_RECEIVER_NAME,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
receivers.push({
|
|
||||||
$: {
|
|
||||||
"android:name": MEDIA_TRANSFER_RECEIVER_NAME,
|
|
||||||
"android:exported": "true",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
mainApplication.receiver = receivers;
|
|
||||||
|
|
||||||
return androidManifest;
|
return androidManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,24 +126,6 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
|||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
}, [event, player]);
|
}, [event, player]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (event !== "subtitleChange") return;
|
|
||||||
return player.subscribeOverlaySubtitle(() => {
|
|
||||||
const sel = player.subtitles.find((s) => s.selected);
|
|
||||||
const cb = callbackRef.current as (s: unknown) => void;
|
|
||||||
cb(
|
|
||||||
sel
|
|
||||||
? {
|
|
||||||
id: sel.id,
|
|
||||||
label: sel.label,
|
|
||||||
language: sel.language,
|
|
||||||
selected: true,
|
|
||||||
}
|
|
||||||
: undefined,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}, [event, player]);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
function createMapper<Key extends keyof OmniPlayerState, State, Result>(
|
||||||
|
|||||||
+9
-10
@@ -216,6 +216,9 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get subtitles(): Track[] {
|
get subtitles(): Track[] {
|
||||||
|
// Every subtitle (incl. ass/pgs) is a real text track now; the cast
|
||||||
|
// receiver draws the ones it can't render natively, keyed off the active
|
||||||
|
// track. So the text-track list already contains all of them.
|
||||||
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
|
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
|
||||||
return textTracks
|
return textTracks
|
||||||
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
|
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
|
||||||
@@ -223,24 +226,20 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
id: track.id!,
|
id: track.id!,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
language: track.language,
|
language: track.language,
|
||||||
selected:
|
selected: track.mode === "showing",
|
||||||
track.mode === "showing" || this.overlaySubtitle?.id === track.id,
|
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSubtitle(subtitle?: Track): void {
|
selectSubtitle(subtitle?: Track): void {
|
||||||
|
// Selecting a text track drives local playback and, while casting,
|
||||||
|
// video.js forwards it to the receiver as an active cast track. ass/pgs
|
||||||
|
// additionally need our local overlay since the browser can't render them.
|
||||||
const tracks = selectTextTrack(this._store.state);
|
const tracks = selectTextTrack(this._store.state);
|
||||||
|
tracks?.selectSubtitlesTrack(subtitle ? subtitle.id : "off");
|
||||||
|
|
||||||
const overlay = subtitle
|
const overlay = subtitle
|
||||||
? this.overlaySubtitles.find((s) => s.id === subtitle.id)
|
? this.overlaySubtitles.find((s) => s.id === subtitle.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
if (this.castStatus === "connected") {
|
|
||||||
tracks?.selectSubtitlesTrack(subtitle ? subtitle.id : "off");
|
|
||||||
this.setOverlaySubtitle(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
|
||||||
this.setOverlaySubtitle(overlay ?? null);
|
this.setOverlaySubtitle(overlay ?? null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user