Compare commits

..
3 Commits
14 changed files with 158 additions and 102 deletions
@@ -177,49 +177,52 @@ class OmniPlayer : HybridOmniPlayerSpec() {
}
}
override fun setSource(src: Source?) {
if (src == null) {
override var source: Source? = null
set(value) {
field = value
if (value == null) {
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
player.prepare()
}
return
}
val handleAudioFocus =
(value.mixAudio ?: MixAudioMode.AUTO) != MixAudioMode.MIXWITHOTHERS
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
runOnMainThread { player.setAudioAttributes(audioAttributes, handleAudioFocus) }
val firstSrc = value.src.firstOrNull()
if (firstSrc == null) {
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
player.prepare()
}
return
}
val currentItem = buildMediaItem(firstSrc, value.metadata, value.subtitles)
val mediaItems = mutableListOf<MediaItem>()
if (value.metadata?.hasPrev == true) {
mediaItems.add(currentItem)
}
mediaItems.add(currentItem)
if (value.metadata?.hasNext == true) {
mediaItems.add(currentItem)
}
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
val startIndex = if (value.metadata?.hasPrev == true) 1 else 0
val startPositionMs = (value.startTime?.coerceAtLeast(0.0) ?: 0.0) * 1000.0
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
player.prepare()
}
return
}
val handleAudioFocus = (src.mixAudio ?: MixAudioMode.AUTO) != MixAudioMode.MIXWITHOTHERS
val audioAttributes = AudioAttributes.Builder()
.setUsage(C.USAGE_MEDIA)
.setContentType(C.AUDIO_CONTENT_TYPE_MOVIE)
.build()
runOnMainThread { player.setAudioAttributes(audioAttributes, handleAudioFocus) }
val source = src.src.firstOrNull()
if (source == null) {
runOnMainThreadSync {
player.setMediaItem(MediaItem.EMPTY)
player.prepare()
}
return
}
val currentItem = buildMediaItem(source, src.metadata, src.subtitles)
val mediaItems = mutableListOf<MediaItem>()
if (src.metadata?.hasPrev == true) {
mediaItems.add(currentItem)
}
mediaItems.add(currentItem)
if (src.metadata?.hasNext == true) {
mediaItems.add(currentItem)
}
runOnMainThreadSync {
val startIndex = if (src.metadata?.hasPrev == true) 1 else 0
val startPositionMs = (src.startTime?.coerceAtLeast(0.0) ?: 0.0) * 1000.0
player.setMediaItems(mediaItems, startIndex, startPositionMs.toLong())
player.prepare()
}
}
override fun play() {
runOnMainThreadSync { player.play() }
@@ -312,6 +315,7 @@ class OmniPlayer : HybridOmniPlayerSpec() {
}
} else null
}
else -> (0 until group.length).firstOrNull { group.isTrackSelected(it) }
}
@@ -347,7 +351,12 @@ class OmniPlayer : HybridOmniPlayerSpec() {
player.trackSelectionParameters = player.trackSelectionParameters
.buildUpon()
.setOverrideForType(TrackSelectionOverride(group.mediaTrackGroup, rendition.id.toInt()))
.setOverrideForType(
TrackSelectionOverride(
group.mediaTrackGroup,
rendition.id.toInt()
)
)
.build()
}
}
@@ -383,7 +392,8 @@ class OmniPlayerService : MediaSessionService() {
.build()
setMediaNotificationProvider(DefaultMediaNotificationProvider.Builder(this).build().apply {
setSmallIcon(applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_media_play)
setSmallIcon(applicationInfo.icon.takeIf { it != 0 }
?: android.R.drawable.ic_media_play)
})
addSession(mediaSession)
setShowNotificationForIdlePlayer(SHOW_NOTIFICATION_FOR_IDLE_PLAYER_ALWAYS)
@@ -415,7 +425,8 @@ class OmniPlayerService : MediaSessionService() {
)
return NotificationCompat.Builder(this, "omni_playback")
.setSmallIcon(applicationInfo.icon.takeIf { it != 0 } ?: android.R.drawable.ic_media_play)
.setSmallIcon(applicationInfo.icon.takeIf { it != 0 }
?: android.R.drawable.ic_media_play)
.setContentTitle("Omni Player")
.setContentText("Preparing playback...")
.setContentIntent(pendingIntent)
@@ -9,7 +9,7 @@ import com.margelo.nitro.omni.Source
class OmniPlayerFactory(val context: ThemedReactContext) : HybridOmniPlayerFactorySpec() {
override fun createPlayer(props: Source?): HybridOmniPlayerSpec {
return OmniPlayer().apply {
setSource(props)
source = props
}
}
}
+11
View File
@@ -12,6 +12,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.5.3",
"@types/chromecast-caf-sender": "^1.0.11",
"@types/jest": "^30.0.0",
"@types/react": "19.2.17",
"nitrogen": "0.36.1",
@@ -596,6 +597,10 @@
"@types/bonjour": ["@types/bonjour@3.5.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ=="],
"@types/chrome": ["@types/chrome@0.2.2", "", { "dependencies": { "@types/filesystem": "*", "@types/har-format": "*" } }, "sha512-8rSMZ4cvo2xmaSyQg0sN5yRL7oiDkntLoiHxUhfwQnv1mvnkrdoZ25SlNrKWmYKaeP50WvrfWj1pmc02+U9KKw=="],
"@types/chromecast-caf-sender": ["@types/chromecast-caf-sender@1.0.11", "", { "dependencies": { "@types/chrome": "*" } }, "sha512-Pv3xvNYtxD/cTM/tKfuZRlLasvpxAm+CFni0GJd6Cp8XgiZS9g9tMZkR1uymsi5fIFv057SZKKAWVFFgy7fJtw=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/connect-history-api-fallback": ["@types/connect-history-api-fallback@1.5.4", "", { "dependencies": { "@types/express-serve-static-core": "*", "@types/node": "*" } }, "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw=="],
@@ -606,6 +611,12 @@
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.8", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA=="],
"@types/filesystem": ["@types/filesystem@0.0.36", "", { "dependencies": { "@types/filewriter": "*" } }, "sha512-vPDXOZuannb9FZdxgHnqSwAG/jvdGM8Wq+6N4D/d80z+D4HWH+bItqsZaVRQykAn6WEVeEkLm2oQigyHtgb0RA=="],
"@types/filewriter": ["@types/filewriter@0.0.33", "", {}, "sha512-xFU8ZXTw4gd358lb2jw25nxY9QAgqn2+bKKjKOYfNCzN4DKCFetK7sPtrlpg66Ywe3vWY9FNxprZawAh9wfJ3g=="],
"@types/har-format": ["@types/har-format@1.2.16", "", {}, "sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A=="],
"@types/html-minifier-terser": ["@types/html-minifier-terser@6.1.0", "", {}, "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
+27 -22
View File
@@ -9,14 +9,6 @@
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
namespace margelo::nitro::omni { struct Rendition; }
// Forward declaration of `PlayerStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class PlayerStatus; }
// Forward declaration of `CastStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class CastStatus; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
// Forward declaration of `VideoSrc` to properly resolve imports.
@@ -27,25 +19,25 @@ namespace margelo::nitro::omni { struct Subtitle; }
namespace margelo::nitro::omni { struct Metadata; }
// Forward declaration of `MixAudioMode` to properly resolve imports.
namespace margelo::nitro::omni { enum class MixAudioMode; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
namespace margelo::nitro::omni { struct Rendition; }
// Forward declaration of `PlayerStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class PlayerStatus; }
// Forward declaration of `CastStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class CastStatus; }
#include <memory>
#include "HybridOmniEventMapSpec.hpp"
#include "JHybridOmniEventMapSpec.hpp"
#include <optional>
#include "Track.hpp"
#include <vector>
#include "JTrack.hpp"
#include <string>
#include "Rendition.hpp"
#include "JRendition.hpp"
#include "PlayerStatus.hpp"
#include "JPlayerStatus.hpp"
#include "CastStatus.hpp"
#include "JCastStatus.hpp"
#include "Source.hpp"
#include "JSource.hpp"
#include "VideoSrc.hpp"
#include <vector>
#include "JVideoSrc.hpp"
#include <string>
#include <unordered_map>
#include "Subtitle.hpp"
#include "JSubtitle.hpp"
@@ -53,6 +45,14 @@ namespace margelo::nitro::omni { enum class MixAudioMode; }
#include "JMetadata.hpp"
#include "MixAudioMode.hpp"
#include "JMixAudioMode.hpp"
#include "Track.hpp"
#include "JTrack.hpp"
#include "Rendition.hpp"
#include "JRendition.hpp"
#include "PlayerStatus.hpp"
#include "JPlayerStatus.hpp"
#include "CastStatus.hpp"
#include "JCastStatus.hpp"
namespace margelo::nitro::omni {
@@ -98,6 +98,15 @@ namespace margelo::nitro::omni {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<jni::JBoolean> /* showNotification */)>("setShowNotification");
method(_javaPart, showNotification.has_value() ? jni::JBoolean::valueOf(showNotification.value()) : nullptr);
}
std::optional<Source> JHybridOmniPlayerSpec::getSource() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jni::local_ref<JSource>()>("getSource");
auto __result = method(_javaPart);
return __result != nullptr ? std::make_optional(__result->toCpp()) : std::nullopt;
}
void JHybridOmniPlayerSpec::setSource(const std::optional<Source>& source) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* source */)>("setSource");
method(_javaPart, source.has_value() ? JSource::fromCpp(source.value()) : nullptr);
}
bool JHybridOmniPlayerSpec::getHasPrev() {
static const auto method = _javaPart->javaClassStatic()->getMethod<jboolean()>("getHasPrev");
auto __result = method(_javaPart);
@@ -232,10 +241,6 @@ namespace margelo::nitro::omni {
}
// Methods
void JHybridOmniPlayerSpec::setSource(const std::optional<Source>& source) {
static const auto method = _javaPart->javaClassStatic()->getMethod<void(jni::alias_ref<JSource> /* source */)>("setSource");
method(_javaPart, source.has_value() ? JSource::fromCpp(source.value()) : nullptr);
}
void JHybridOmniPlayerSpec::play() {
static const auto method = _javaPart->javaClassStatic()->getMethod<void()>("play");
method(_javaPart);
+2 -1
View File
@@ -53,6 +53,8 @@ namespace margelo::nitro::omni {
std::shared_ptr<HybridOmniEventMapSpec> getEventMap() override;
std::optional<bool> getShowNotification() override;
void setShowNotification(std::optional<bool> showNotification) override;
std::optional<Source> getSource() override;
void setSource(const std::optional<Source>& source) override;
bool getHasPrev() override;
bool getHasNext() override;
std::vector<Track> getVideos() override;
@@ -76,7 +78,6 @@ namespace margelo::nitro::omni {
public:
// Methods
void setSource(const std::optional<Source>& source) override;
void play() override;
void pause() override;
void seekBy(double offset) override;
@@ -35,6 +35,12 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
@set:Keep
abstract var showNotification: Boolean?
@get:DoNotStrip
@get:Keep
@set:DoNotStrip
@set:Keep
abstract var source: Source?
@get:DoNotStrip
@get:Keep
abstract val hasPrev: Boolean
@@ -108,10 +114,6 @@ abstract class HybridOmniPlayerSpec: HybridObject() {
abstract val castStatus: CastStatus
// Methods
@DoNotStrip
@Keep
abstract fun setSource(source: Source?): Unit
@DoNotStrip
@Keep
abstract fun play(): Unit
+2 -1
View File
@@ -17,6 +17,8 @@ namespace margelo::nitro::omni {
prototype.registerHybridGetter("eventMap", &HybridOmniPlayerSpec::getEventMap);
prototype.registerHybridGetter("showNotification", &HybridOmniPlayerSpec::getShowNotification);
prototype.registerHybridSetter("showNotification", &HybridOmniPlayerSpec::setShowNotification);
prototype.registerHybridGetter("source", &HybridOmniPlayerSpec::getSource);
prototype.registerHybridSetter("source", &HybridOmniPlayerSpec::setSource);
prototype.registerHybridGetter("hasPrev", &HybridOmniPlayerSpec::getHasPrev);
prototype.registerHybridGetter("hasNext", &HybridOmniPlayerSpec::getHasNext);
prototype.registerHybridGetter("videos", &HybridOmniPlayerSpec::getVideos);
@@ -37,7 +39,6 @@ namespace margelo::nitro::omni {
prototype.registerHybridSetter("muted", &HybridOmniPlayerSpec::setMuted);
prototype.registerHybridGetter("isAutoQuality", &HybridOmniPlayerSpec::getIsAutoQuality);
prototype.registerHybridGetter("castStatus", &HybridOmniPlayerSpec::getCastStatus);
prototype.registerHybridMethod("setSource", &HybridOmniPlayerSpec::setSource);
prototype.registerHybridMethod("play", &HybridOmniPlayerSpec::play);
prototype.registerHybridMethod("pause", &HybridOmniPlayerSpec::pause);
prototype.registerHybridMethod("seekBy", &HybridOmniPlayerSpec::seekBy);
+5 -4
View File
@@ -15,6 +15,8 @@
// Forward declaration of `HybridOmniEventMapSpec` to properly resolve imports.
namespace margelo::nitro::omni { class HybridOmniEventMapSpec; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
// Forward declaration of `Track` to properly resolve imports.
namespace margelo::nitro::omni { struct Track; }
// Forward declaration of `Rendition` to properly resolve imports.
@@ -23,18 +25,16 @@ namespace margelo::nitro::omni { struct Rendition; }
namespace margelo::nitro::omni { enum class PlayerStatus; }
// Forward declaration of `CastStatus` to properly resolve imports.
namespace margelo::nitro::omni { enum class CastStatus; }
// Forward declaration of `Source` to properly resolve imports.
namespace margelo::nitro::omni { struct Source; }
#include <memory>
#include "HybridOmniEventMapSpec.hpp"
#include <optional>
#include "Source.hpp"
#include "Track.hpp"
#include <vector>
#include "Rendition.hpp"
#include "PlayerStatus.hpp"
#include "CastStatus.hpp"
#include "Source.hpp"
namespace margelo::nitro::omni {
@@ -66,6 +66,8 @@ namespace margelo::nitro::omni {
virtual std::shared_ptr<HybridOmniEventMapSpec> getEventMap() = 0;
virtual std::optional<bool> getShowNotification() = 0;
virtual void setShowNotification(std::optional<bool> showNotification) = 0;
virtual std::optional<Source> getSource() = 0;
virtual void setSource(const std::optional<Source>& source) = 0;
virtual bool getHasPrev() = 0;
virtual bool getHasNext() = 0;
virtual std::vector<Track> getVideos() = 0;
@@ -89,7 +91,6 @@ namespace margelo::nitro::omni {
public:
// Methods
virtual void setSource(const std::optional<Source>& source) = 0;
virtual void play() = 0;
virtual void pause() = 0;
virtual void seekBy(double offset) = 0;
+1
View File
@@ -69,6 +69,7 @@
},
"devDependencies": {
"@biomejs/biome": "2.5.3",
"@types/chromecast-caf-sender": "^1.0.11",
"@types/jest": "^30.0.0",
"@types/react": "19.2.17",
"nitrogen": "0.36.1",
+16 -24
View File
@@ -61,7 +61,7 @@ export class WebOmniPlayer implements OmniPlayer {
}
castOptions: CastOptions | null = null;
_source: Source | null = null;
_source: Source | undefined = undefined;
private _showNotification = false;
// Selected ASS/PGS subtitle (drawn by the overlay); `null` when the active
@@ -70,15 +70,11 @@ export class WebOmniPlayer implements OmniPlayer {
private overlaySubtitle: Subtitle | null = null;
private overlayListeners = new Set<() => void>();
get source(): Source | null {
get source(): Source | undefined {
return this._source;
}
setSource(source?: Source): void {
this.source = source ?? null;
}
set source(source: Source | null) {
set source(source: Source | undefined) {
this._source = source;
// Drop the overlay subtitle if it is not part of the new source.
if (
@@ -248,26 +244,22 @@ export class WebOmniPlayer implements OmniPlayer {
this.setOverlaySubtitle(overlay ?? null);
if (this.castStatus === "connected") {
try {
// if the cast receiver handles overlay subtitles like us, send this message.
const ctx = (
window as unknown as {
// biome-ignore lint/suspicious/noExplicitAny: cast sender SDK global
cast?: { framework?: { CastContext?: { getInstance(): any } } };
}
).cast?.framework?.CastContext?.getInstance?.();
ctx
?.getCurrentSession?.()
?.sendMessage("urn:x-cast:dev.zoriya.omni", {
subtitle: overlay?.id ?? null,
})
?.catch?.(() => {});
} catch {
// no active cast session / sender SDK not loaded
}
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 {
if (this.overlaySubtitle === sub) return;
this.overlaySubtitle = sub;
+1 -1
View File
@@ -24,7 +24,7 @@ export const OmniProvider = ({
const player = useLazyRef(() => ProviderFactory.createPlayer(source));
useEffect(() => {
player.setSource(source);
player.source = source;
}, [source]);
useEffect(() => {
+31 -1
View File
@@ -56,7 +56,7 @@ const PlayerInitializer = ({
const seekedForSrc = useRef<string | undefined>(undefined);
useEffect(() => {
player.source = source ?? null;
player.source = source;
const uri = source?.src[0]?.uri;
if (uri !== seekedForSrc.current) {
seekedForSrc.current = uri;
@@ -72,6 +72,36 @@ const PlayerInitializer = ({
player.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>;
};
+1 -1
View File
@@ -3,7 +3,7 @@ import type { Source } from "./source";
export interface OmniPlayer extends OmniPlayerState {
showNotification?: boolean;
setSource(source?: Source): void;
source?: Source;
play(): void;
pause(): void;
+1
View File
@@ -17,6 +17,7 @@
"skipLibCheck": true,
"strict": true,
"target": "esnext",
"types": ["chromecast-caf-sender"],
"verbatimModuleSyntax": true
},
"exclude": [