mirror of
https://github.com/zoriya/react-native-omni.git
synced 2026-08-16 02:44:51 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3739e760cf | ||
|
|
099eeabe18 | ||
|
|
d8f96962e3 | ||
|
|
12be950ea0 |
@@ -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:21.5.0")
|
implementation("com.google.android.gms:play-services-cast-framework:22.3.1")
|
||||||
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
implementation("androidx.mediarouter:mediarouter:1.7.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class OmniCastOptionsProvider : OptionsProvider {
|
|||||||
|
|
||||||
return CastOptions.Builder()
|
return CastOptions.Builder()
|
||||||
.setReceiverApplicationId(appId)
|
.setReceiverApplicationId(appId)
|
||||||
|
.setShowSystemOutputSwitcherOnCastIconClick(true)
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ class OmniCastTrackSelector : CastTrackSelector() {
|
|||||||
indices.forEach { selections.remove(groups[it]) }
|
indices.forEach { selections.remove(groups[it]) }
|
||||||
if (disabled) return
|
if (disabled) return
|
||||||
|
|
||||||
|
val overrides = request.trackSelectionParameters.overrides
|
||||||
|
val overrideMatch = indices.firstOrNull { i ->
|
||||||
|
overrides.keys.any { it.id == groups[i].id }
|
||||||
|
}
|
||||||
val match = indices.firstOrNull { i ->
|
val match = indices.firstOrNull { i ->
|
||||||
val name: String? = tracks[i].name
|
val name: String? = tracks[i].name
|
||||||
val language: String? = tracks[i].language
|
val language: String? = tracks[i].language
|
||||||
@@ -64,6 +68,7 @@ class OmniCastTrackSelector : CastTrackSelector() {
|
|||||||
Util.normalizeLanguageCode(language) in languages)
|
Util.normalizeLanguageCode(language) in languages)
|
||||||
}
|
}
|
||||||
val chosen = when {
|
val chosen = when {
|
||||||
|
overrideMatch != null -> overrideMatch
|
||||||
match != null -> match
|
match != null -> match
|
||||||
previous.isNotEmpty() -> previous.first()
|
previous.isNotEmpty() -> previous.first()
|
||||||
selectDefault -> indices.first()
|
selectDefault -> indices.first()
|
||||||
|
|||||||
@@ -43,8 +43,14 @@ 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 { putString(CastMediaMetadata.KEY_ARTIST, it.toString()) }
|
meta.artist?.let {
|
||||||
meta.albumTitle?.let { putString(CastMediaMetadata.KEY_ALBUM_TITLE, it.toString()) }
|
putString(CastMediaMetadata.KEY_ARTIST, 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)) }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,7 +63,8 @@ class OmniMediaItemConverter : MediaItemConverter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val tracks = local.subtitleConfigurations.mapIndexed { index, subtitle ->
|
val tracks = local.subtitleConfigurations.mapIndexed { index, subtitle ->
|
||||||
MediaTrack.Builder(index.toLong(), MediaTrack.TYPE_TEXT)
|
// caf tracks are 1 based instead of 0 based. videojs does the same
|
||||||
|
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,6 +37,7 @@ 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 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
|
||||||
@@ -124,6 +125,9 @@ class OmniPlayer(
|
|||||||
override val castStatus: CastStatus
|
override val castStatus: CastStatus
|
||||||
get() = runOnMainThreadSync { computeCastStatus() }
|
get() = runOnMainThreadSync { computeCastStatus() }
|
||||||
|
|
||||||
|
val isCasting: Boolean
|
||||||
|
get() = castContext?.sessionManager?.currentCastSession?.isConnected == true
|
||||||
|
|
||||||
private fun computeCastStatus(): CastStatus {
|
private fun computeCastStatus(): CastStatus {
|
||||||
val cc = castContext ?: return CastStatus.UNSUPPORTED
|
val cc = castContext ?: return CastStatus.UNSUPPORTED
|
||||||
return when (cc.castState) {
|
return when (cc.castState) {
|
||||||
@@ -144,6 +148,9 @@ 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()
|
||||||
}
|
}
|
||||||
@@ -358,55 +365,35 @@ class OmniPlayer(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun selectAudio(audio: Track) {
|
override fun selectAudio(audio: Track) = selectTrack(C.TRACK_TYPE_AUDIO, audio)
|
||||||
runOnMainThreadSync {
|
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
|
||||||
.buildUpon()
|
|
||||||
.setPreferredAudioLanguage(audio.language)
|
|
||||||
.setPreferredAudioLabels(*(audio.label?.let { arrayOf(it) } ?: emptyArray()))
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun selectSubtitle(subtitle: Track?) {
|
override fun selectSubtitle(subtitle: Track?) = selectTrack(C.TRACK_TYPE_TEXT, subtitle)
|
||||||
// Custom (ASS/PGS) subtitles can't be rendered by the cast receiver's
|
|
||||||
// native pipeline, so while casting they are forwarded over omni's
|
private fun selectTrack(type: Int, track: Track?) {
|
||||||
// message channel (the receiver draws them as an overlay). This mirrors
|
|
||||||
// the web behavior.
|
|
||||||
val custom = subtitle?.let { track ->
|
|
||||||
source?.subtitles?.firstOrNull { it.id == track.id }?.let { sub ->
|
|
||||||
val mime = sub.mimeType?.lowercase() ?: ""
|
|
||||||
val ext = sub.link.substringBefore('?').substringBefore('#')
|
|
||||||
.substringAfterLast('.', "").lowercase()
|
|
||||||
mime.contains("ass") || mime.contains("ssa") || mime.contains("pgs") ||
|
|
||||||
ext == "ass" || ext == "ssa" || ext == "sup"
|
|
||||||
}
|
|
||||||
} ?: false
|
|
||||||
runOnMainThreadSync {
|
runOnMainThreadSync {
|
||||||
val session = castContext?.sessionManager?.currentCastSession
|
val params = player.trackSelectionParameters.buildUpon()
|
||||||
val casting = session?.isConnected == true
|
.clearOverridesOfType(type)
|
||||||
if (subtitle == null || (custom && casting)) {
|
if (track == null) {
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
params.setTrackTypeDisabled(type, true)
|
||||||
.buildUpon()
|
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true)
|
|
||||||
.build()
|
|
||||||
} else {
|
} else {
|
||||||
player.trackSelectionParameters = player.trackSelectionParameters
|
params.setTrackTypeDisabled(type, false)
|
||||||
.buildUpon()
|
val group = player.currentTracks.groups.firstOrNull {
|
||||||
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
|
it.type == type && it.mediaTrackGroup.id == track.id
|
||||||
.setPreferredTextLanguage(subtitle.language)
|
}?.mediaTrackGroup
|
||||||
.setPreferredTextLabels(*(subtitle.label?.let { arrayOf(it) } ?: emptyArray()))
|
if (group != null) {
|
||||||
.build()
|
params.setOverrideForType(TrackSelectionOverride(group, 0))
|
||||||
}
|
} else {
|
||||||
if (casting) {
|
val labels = track.label?.let { arrayOf(it) } ?: emptyArray()
|
||||||
val id = if (custom) subtitle?.id else null
|
if (type == C.TRACK_TYPE_AUDIO) {
|
||||||
val payload = JSONObject().apply { put("subtitle", id ?: JSONObject.NULL) }
|
params.setPreferredAudioLanguage(track.language)
|
||||||
try {
|
.setPreferredAudioLabels(*labels)
|
||||||
session?.sendMessage(CAST_MESSAGE_NAMESPACE, payload.toString())
|
} else {
|
||||||
} catch (_: Throwable) {
|
params.setPreferredTextLanguage(track.language)
|
||||||
// best effort; receiver may not support the namespace.
|
.setPreferredTextLabels(*labels)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
player.trackSelectionParameters = params.build()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -414,18 +401,30 @@ class OmniPlayer(
|
|||||||
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()
|
||||||
|
|
||||||
return groups.map {
|
// when casting tracks are stripped of some metadata, recover it
|
||||||
it.getTrackFormat(0).run {
|
val castTracks = castMediaTracksById()
|
||||||
Track(
|
|
||||||
id = it.mediaTrackGroup.id,
|
return groups.map { group ->
|
||||||
label = this.label,
|
val format = group.getTrackFormat(0)
|
||||||
language = this.language,
|
|
||||||
selected = it.isSelected
|
val castTrackId = group.mediaTrackGroup.id.substringAfterLast("track=", "").toLongOrNull()
|
||||||
)
|
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 }
|
||||||
@@ -489,10 +488,6 @@ class OmniPlayer(
|
|||||||
companion object {
|
companion object {
|
||||||
var notificationPlayer: Player? = null
|
var notificationPlayer: Player? = null
|
||||||
|
|
||||||
// Cast custom-message channel (shared with the web receiver) used to
|
|
||||||
// forward overlay (ASS/PGS) subtitle selection to the receiver.
|
|
||||||
const val CAST_MESSAGE_NAMESPACE = "urn:x-cast:dev.zoriya.omni"
|
|
||||||
|
|
||||||
// MediaItem RequestMetadata extras keys carrying cast-only data.
|
// MediaItem RequestMetadata extras keys carrying cast-only data.
|
||||||
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
|
const val CAST_ID_EXTRA = "dev.zoriya.omni.castId"
|
||||||
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
|
const val CAST_DATA_EXTRA = "dev.zoriya.omni.castData"
|
||||||
|
|||||||
@@ -185,7 +185,10 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boundPlayer == null || boundPlayer?.localPlayer?.isPlaying != true) {
|
if (boundPlayer == null ||
|
||||||
|
boundPlayer?.localPlayer?.isPlaying != true ||
|
||||||
|
boundPlayer?.isCasting == true
|
||||||
|
) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +222,8 @@ class OmniView(val context: ThemedReactContext) :
|
|||||||
val autoEnterEnabled =
|
val autoEnterEnabled =
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S &&
|
||||||
autoPip == true &&
|
autoPip == true &&
|
||||||
boundPlayer?.localPlayer?.isPlaying == true
|
boundPlayer?.localPlayer?.isPlaying == true &&
|
||||||
|
boundPlayer?.isCasting != true
|
||||||
activity.setPictureInPictureParams(buildPipParams(autoEnterEnabled))
|
activity.setPictureInPictureParams(buildPipParams(autoEnterEnabled))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+9
-5
@@ -1,5 +1,5 @@
|
|||||||
import type React from "react";
|
import type React from "react";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, 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,
|
||||||
@@ -75,7 +75,8 @@ 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<string[]>([]);
|
const [logs, setLogs] = useState<{ id: number; message: 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],
|
||||||
@@ -98,7 +99,7 @@ function PlayerExample({
|
|||||||
|
|
||||||
const pushLog = useCallback((message: string) => {
|
const pushLog = useCallback((message: string) => {
|
||||||
setLogs((prev) => {
|
setLogs((prev) => {
|
||||||
const next = [message, ...prev];
|
const next = [{ id: logId.current++, message }, ...prev];
|
||||||
return next.slice(0, 8);
|
return next.slice(0, 8);
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
@@ -484,8 +485,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}`} style={styles.logText}>
|
<Text key={entry.id} style={styles.logText}>
|
||||||
{entry}
|
{entry.message}
|
||||||
</Text>
|
</Text>
|
||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
@@ -536,6 +537,8 @@ 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",
|
||||||
@@ -573,6 +576,7 @@ function App(): React.JSX.Element {
|
|||||||
key={backend}
|
key={backend}
|
||||||
source={source}
|
source={source}
|
||||||
backend={{ android: backend }}
|
backend={{ android: backend }}
|
||||||
|
cast={{receiverApplicationId: "D8FB0FC1"}}
|
||||||
showNotification
|
showNotification
|
||||||
>
|
>
|
||||||
<PlayerExample
|
<PlayerExample
|
||||||
|
|||||||
@@ -28,5 +28,8 @@
|
|||||||
<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>
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ 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,
|
||||||
@@ -36,6 +38,21 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -127,9 +127,6 @@ export const useEvent = <Event extends keyof OmniEvents>(
|
|||||||
return undefined;
|
return undefined;
|
||||||
}, [event, player]);
|
}, [event, player]);
|
||||||
|
|
||||||
// ASS/PGS (overlay) subtitle selection is not a videojs text-track change, so
|
|
||||||
// re-fire subtitleChange when the overlay subtitle changes too — consumers
|
|
||||||
// only need useEvent("subtitleChange").
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (event !== "subtitleChange") return;
|
if (event !== "subtitleChange") return;
|
||||||
return player.subscribeOverlaySubtitle(() => {
|
return player.subscribeOverlaySubtitle(() => {
|
||||||
|
|||||||
+10
-26
@@ -217,47 +217,31 @@ export class WebOmniPlayer implements OmniPlayer {
|
|||||||
|
|
||||||
get subtitles(): Track[] {
|
get subtitles(): Track[] {
|
||||||
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
|
const textTracks = selectTextTrack(this._store.state)?.textTrackList ?? [];
|
||||||
const native = textTracks
|
return textTracks
|
||||||
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
|
.filter((x) => x.kind === "subtitles" || x.kind === "captions")
|
||||||
.map((track) => ({
|
.map((track) => ({
|
||||||
id: track.id!,
|
id: track.id!,
|
||||||
label: track.label,
|
label: track.label,
|
||||||
language: track.language,
|
language: track.language,
|
||||||
selected: track.mode === "showing",
|
selected:
|
||||||
|
track.mode === "showing" || this.overlaySubtitle?.id === track.id,
|
||||||
}));
|
}));
|
||||||
const overlay = this.overlaySubtitles.map((sub) => ({
|
|
||||||
id: sub.id,
|
|
||||||
label: sub.label,
|
|
||||||
language: sub.language,
|
|
||||||
selected: this.overlaySubtitle?.id === sub.id,
|
|
||||||
}));
|
|
||||||
return [...native, ...overlay];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectSubtitle(subtitle?: Track): void {
|
selectSubtitle(subtitle?: Track): void {
|
||||||
|
const tracks = selectTextTrack(this._store.state);
|
||||||
const overlay = subtitle
|
const overlay = subtitle
|
||||||
? this.overlaySubtitles.find((s) => s.id === subtitle.id)
|
? this.overlaySubtitles.find((s) => s.id === subtitle.id)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const tracks = selectTextTrack(this._store.state);
|
if (this.castStatus === "connected") {
|
||||||
|
tracks?.selectSubtitlesTrack(subtitle ? subtitle.id : "off");
|
||||||
|
this.setOverlaySubtitle(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
tracks?.selectSubtitlesTrack(overlay || !subtitle ? "off" : subtitle.id);
|
||||||
this.setOverlaySubtitle(overlay ?? null);
|
this.setOverlaySubtitle(overlay ?? null);
|
||||||
|
|
||||||
if (this.castStatus === "connected") {
|
|
||||||
window.cast.framework.CastContext.getInstance()
|
|
||||||
.getCurrentSession()
|
|
||||||
?.sendMessage("urn:x-cast:dev.zoriya.omni", {
|
|
||||||
subtitle: overlay?.id ?? null,
|
|
||||||
})
|
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// To be used on a callback of a chromecast session, it only changes the local state
|
|
||||||
applyRemoteSubtitle(id: string | null): void {
|
|
||||||
this.setOverlaySubtitle(
|
|
||||||
id ? (this.overlaySubtitles.find((s) => s.id === id) ?? null) : null,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private setOverlaySubtitle(sub: Subtitle | null): void {
|
private setOverlaySubtitle(sub: Subtitle | null): void {
|
||||||
|
|||||||
@@ -75,36 +75,6 @@ const PlayerInitializer = ({
|
|||||||
player.showNotification = showNotification;
|
player.showNotification = showNotification;
|
||||||
}, [showNotification]);
|
}, [showNotification]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let media: chrome.cast.media.Media | null = null;
|
|
||||||
|
|
||||||
const onMediaUpdate = () => {
|
|
||||||
const data = media?.customData as { subtitle?: unknown } | undefined;
|
|
||||||
player.applyRemoteSubtitle(
|
|
||||||
typeof data?.subtitle === "string" ? data.subtitle : null,
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const syncMedia = () => {
|
|
||||||
const next =
|
|
||||||
window.cast.framework.CastContext.getInstance()
|
|
||||||
.getCurrentSession()
|
|
||||||
?.getMediaSession() ?? null;
|
|
||||||
if (next === media) return;
|
|
||||||
media?.removeUpdateListener(onMediaUpdate);
|
|
||||||
media = next;
|
|
||||||
media?.addUpdateListener(onMediaUpdate);
|
|
||||||
onMediaUpdate();
|
|
||||||
};
|
|
||||||
|
|
||||||
const unsubscribe = store.subscribe(syncMedia);
|
|
||||||
syncMedia();
|
|
||||||
return () => {
|
|
||||||
unsubscribe();
|
|
||||||
media?.removeUpdateListener(onMediaUpdate);
|
|
||||||
};
|
|
||||||
}, [store]);
|
|
||||||
|
|
||||||
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
return <PlayerCtx.Provider value={player}>{children}</PlayerCtx.Provider>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+11
-17
@@ -10,11 +10,7 @@ import {
|
|||||||
useSyncExternalStore,
|
useSyncExternalStore,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { usePlayerState } from "./events";
|
import { usePlayerState } from "./events";
|
||||||
import {
|
import { getSubtitleFormat, type WebOmniPlayer } from "./player.web";
|
||||||
getSubtitleFormat,
|
|
||||||
isCustomSubtitle,
|
|
||||||
type WebOmniPlayer,
|
|
||||||
} from "./player.web";
|
|
||||||
import { usePlayer, VideoPlayer } from "./provider.web";
|
import { usePlayer, VideoPlayer } from "./provider.web";
|
||||||
import type { SubtitleAssets } from "./types/subtitles";
|
import type { SubtitleAssets } from "./types/subtitles";
|
||||||
import type { OmniViewProps } from "./types/view";
|
import type { OmniViewProps } from "./types/view";
|
||||||
@@ -165,18 +161,16 @@ export const OmniView = ({
|
|||||||
crossOrigin="anonymous"
|
crossOrigin="anonymous"
|
||||||
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
style={{ width: "100%", height: "100%", objectFit: "contain" }}
|
||||||
>
|
>
|
||||||
{(player.source?.subtitles ?? [])
|
{(player.source?.subtitles ?? []).map((subtitle) => (
|
||||||
.filter((subtitle) => !isCustomSubtitle(subtitle))
|
<track
|
||||||
.map((subtitle) => (
|
key={subtitle.id}
|
||||||
<track
|
id={subtitle.id}
|
||||||
key={subtitle.id}
|
kind="subtitles"
|
||||||
id={subtitle.id}
|
src={subtitle.link}
|
||||||
kind="subtitles"
|
srcLang={subtitle.language}
|
||||||
src={subtitle.link}
|
label={subtitle.label ?? subtitle.language ?? subtitle.id}
|
||||||
srcLang={subtitle.language}
|
/>
|
||||||
label={subtitle.label ?? subtitle.language ?? subtitle.id}
|
))}
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Tech>
|
</Tech>
|
||||||
)}
|
)}
|
||||||
{castStatus !== "connected" && castStatus !== "connecting" && (
|
{castStatus !== "connected" && castStatus !== "connecting" && (
|
||||||
|
|||||||
Reference in New Issue
Block a user