This commit is contained in:
Fredrik Burmester
2024-10-19 21:20:11 +02:00
parent f5b05bf32d
commit d9fde3ba79
9 changed files with 211 additions and 157 deletions

View File

@@ -34,7 +34,7 @@ const Page: React.FC = () => {
return res.data; return res.data;
}, },
enabled: !!id && !!api, enabled: !!id && !!api || !!user,
staleTime: 0, staleTime: 0,
}); });

View File

@@ -230,6 +230,8 @@ export default function page() {
const { currentTime, isPlaying } = data.nativeEvent; const { currentTime, isPlaying } = data.nativeEvent;
console.log("onProgress", currentTime);
progress.value = currentTime; progress.value = currentTime;
const currentTimeInTicks = msToTicks(currentTime); const currentTimeInTicks = msToTicks(currentTime);

View File

@@ -52,6 +52,10 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
const [loadingLogo, setLoadingLogo] = useState(true); const [loadingLogo, setLoadingLogo] = useState(true);
const [headerHeight, setHeaderHeight] = useState(350); const [headerHeight, setHeaderHeight] = useState(350);
const [selectedOptions, setSelectedOptions] = useState<
SelectedOptions | undefined
>(undefined);
const { const {
defaultAudioIndex, defaultAudioIndex,
defaultBitrate, defaultBitrate,
@@ -59,12 +63,19 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
defaultSubtitleIndex, defaultSubtitleIndex,
} = useDefaultPlaySettings(item, settings); } = useDefaultPlaySettings(item, settings);
const [selectedOptions, setSelectedOptions] = useState<SelectedOptions>({ useEffect(() => {
bitrate: defaultBitrate, setSelectedOptions(() => ({
mediaSource: defaultMediaSource, bitrate: defaultBitrate,
audioIndex: defaultAudioIndex, mediaSource: defaultMediaSource,
subtitleIndex: defaultSubtitleIndex || -1, subtitleIndex: defaultSubtitleIndex ?? -1,
}); audioIndex: defaultAudioIndex,
}));
}, [
defaultAudioIndex,
defaultBitrate,
defaultSubtitleIndex,
defaultMediaSource,
]);
useEffect(() => { useEffect(() => {
navigation.setOptions({ navigation.setOptions({
@@ -96,6 +107,8 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
return Boolean(logoUrl && loadingLogo); return Boolean(logoUrl && loadingLogo);
}, [loadingLogo, logoUrl]); }, [loadingLogo, logoUrl]);
if (!selectedOptions) return null;
return ( return (
<View <View
className="flex-1 relative" className="flex-1 relative"
@@ -148,7 +161,9 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
<BitrateSelector <BitrateSelector
className="mr-1" className="mr-1"
onChange={(val) => onChange={(val) =>
setSelectedOptions((prev) => ({ ...prev, bitrate: val })) setSelectedOptions(
(prev) => prev && { ...prev, bitrate: val }
)
} }
selected={selectedOptions.bitrate} selected={selectedOptions.bitrate}
/> />
@@ -156,10 +171,13 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
className="mr-1" className="mr-1"
item={item} item={item}
onChange={(val) => onChange={(val) =>
setSelectedOptions((prev) => ({ setSelectedOptions(
...prev, (prev) =>
mediaSource: val, prev && {
})) ...prev,
mediaSource: val,
}
)
} }
selected={selectedOptions.mediaSource} selected={selectedOptions.mediaSource}
/> />
@@ -167,20 +185,26 @@ export const ItemContent: React.FC<{ item: BaseItemDto }> = React.memo(
className="mr-1" className="mr-1"
source={selectedOptions.mediaSource} source={selectedOptions.mediaSource}
onChange={(val) => onChange={(val) =>
setSelectedOptions((prev) => ({ setSelectedOptions(
...prev, (prev) =>
audioIndex: val, prev && {
})) ...prev,
audioIndex: val,
}
)
} }
selected={selectedOptions.audioIndex} selected={selectedOptions.audioIndex}
/> />
<SubtitleTrackSelector <SubtitleTrackSelector
source={selectedOptions.mediaSource} source={selectedOptions.mediaSource}
onChange={(val) => onChange={(val) =>
setSelectedOptions((prev) => ({ setSelectedOptions(
...prev, (prev) =>
subtitleIndex: val, prev && {
})) ...prev,
subtitleIndex: val,
}
)
} }
selected={selectedOptions.subtitleIndex} selected={selectedOptions.subtitleIndex}
/> />

View File

@@ -26,7 +26,7 @@ export const MediaSourceSelector: React.FC<Props> = ({
item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find( item.MediaSources?.find((x) => x.Id === selected?.Id)?.MediaStreams?.find(
(x) => x.Type === "Video" (x) => x.Type === "Video"
)?.DisplayTitle || "", )?.DisplayTitle || "",
[item.MediaSources, selected] [item, selected]
); );
return ( return (

View File

@@ -42,7 +42,11 @@ const useDefaultPlaySettings = (
defaultMediaSource: mediaSource || undefined, defaultMediaSource: mediaSource || undefined,
defaultBitrate: bitrate || undefined, defaultBitrate: bitrate || undefined,
}; };
}, [item, settings]); }, [
item.MediaSources,
settings?.defaultAudioLanguage,
settings?.defaultSubtitleLanguage,
]);
return playSettings; return playSettings;
}; };

View File

@@ -54,7 +54,6 @@ export const useImageColors = ({
// If colors are cached, use them and exit // If colors are cached, use them and exit
if (_primary && _text) { if (_primary && _text) {
console.info("useImageColors ~ Using cached colors for performance.");
setPrimaryColor({ setPrimaryColor({
primary: _primary, primary: _primary,
text: _text, text: _text,

View File

@@ -10,7 +10,7 @@ Pod::Spec.new do |s|
s.static_framework = true s.static_framework = true
s.dependency 'ExpoModulesCore' s.dependency 'ExpoModulesCore'
s.dependency 'MobileVLCKit' s.dependency 'MobileVLCKit', '~> 3.6.1b1'
# Swift/Objective-C compatibility # Swift/Objective-C compatibility
s.pod_target_xcconfig = { s.pod_target_xcconfig = {

View File

@@ -16,17 +16,17 @@ public class VlcPlayerModule: Module {
} }
} }
Prop("muted") { (view: VlcPlayerView, muted: Bool) in // Prop("muted") { (view: VlcPlayerView, muted: Bool) in
view.setMuted(muted) // view.setMuted(muted)
} // }
Prop("volume") { (view: VlcPlayerView, volume: Int) in // Prop("volume") { (view: VlcPlayerView, volume: Int) in
view.setVolume(volume) // view.setVolume(volume)
} // }
Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in // Prop("videoAspectRatio") { (view: VlcPlayerView, ratio: String) in
view.setVideoAspectRatio(ratio) // view.setVideoAspectRatio(ratio)
} // }
Events( Events(
"onPlaybackStateChanged", "onPlaybackStateChanged",
@@ -69,13 +69,13 @@ public class VlcPlayerModule: Module {
return view.getSubtitleTracks() return view.getSubtitleTracks()
} }
AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in // AsyncFunction("setVideoCropGeometry") { (view: VlcPlayerView, geometry: String?) in
view.setVideoCropGeometry(geometry) // view.setVideoCropGeometry(geometry)
} // }
AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in // AsyncFunction("getVideoCropGeometry") { (view: VlcPlayerView) -> String? in
return view.getVideoCropGeometry() // return view.getVideoCropGeometry()
} // }
AsyncFunction("setSubtitleURL") { AsyncFunction("setSubtitleURL") {
(view: VlcPlayerView, url: String, name: String) in (view: VlcPlayerView, url: String, name: String) in

View File

@@ -18,7 +18,7 @@ class VlcPlayerView: ExpoView {
required init(appContext: AppContext? = nil) { required init(appContext: AppContext? = nil) {
super.init(appContext: appContext) super.init(appContext: appContext)
setupView() setupView()
setupNotifications() // setupNotifications()
} }
// MARK: - Setup // MARK: - Setup
@@ -68,6 +68,7 @@ class VlcPlayerView: ExpoView {
guard let self = self else { return } guard let self = self else { return }
self.mediaPlayer?.play() self.mediaPlayer?.play()
self.isPaused = false self.isPaused = false
print("Play")
} }
} }
@@ -143,6 +144,7 @@ class VlcPlayerView: ExpoView {
media = VLCMedia(url: url) media = VLCMedia(url: url)
} else { } else {
print("Error: Invalid local file URL") print("Error: Invalid local file URL")
self.onVideoError?(["error": "Invalid local file URL"])
return return
} }
} else { } else {
@@ -155,13 +157,13 @@ class VlcPlayerView: ExpoView {
media.addOptions(subtitleOptions) media.addOptions(subtitleOptions)
print("Debug: Applied subtitle options: \(subtitleOptions)") print("Debug: Applied subtitle options: \(subtitleOptions)")
// Apply any additional media options // // Apply any additional media options
if let mediaOptions = mediaOptions { // if let mediaOptions = mediaOptions {
media.addOptions(mediaOptions) // media.addOptions(mediaOptions)
print("Debug: Applied additional media options: \(mediaOptions)") // print("Debug: Applied additional media options: \(mediaOptions)")
} else { // } else {
print("Debug: No additional media options provided") // print("Debug: No additional media options provided")
} // }
// Apply subtitle options // Apply subtitle options
let subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?? -1 let subtitleTrackIndex = source["subtitleTrackIndex"] as? Int ?? -1
@@ -177,19 +179,30 @@ class VlcPlayerView: ExpoView {
self.mediaPlayer?.media = media self.mediaPlayer?.media = media
if startPosition > 0 { if startPosition > 0 {
// Wait for the media to be ready before setting the start position // Create a closure to set the start position
NotificationCenter.default.addObserver( let setStartPosition = { [weak self] in
forName: NSNotification.Name(rawValue: VLCMediaPlayerStateChanged), object: nil, self?.mediaPlayer?.time = VLCTime(int: startPosition)
queue: nil }
) { [weak self] notification in
guard let self = self, let player = self.mediaPlayer,
player.isPlaying == false
else { return }
self.mediaPlayer?.time = VLCTime(int: startPosition) // Check if the media is already ready
NotificationCenter.default.removeObserver( if self.isMediaReady {
self, name: NSNotification.Name(rawValue: VLCMediaPlayerStateChanged), setStartPosition()
object: nil) } else {
// If not ready, set up an observer to wait for the media to be ready
NotificationCenter.default.addObserver(
forName: .VLCMediaPlayerStateChanged, object: self.mediaPlayer, queue: .main
) { [weak self] notification in
guard let self = self, let player = self.mediaPlayer else { return }
if player.state == .playing || player.state == .paused {
// Media is ready, set the start position
setStartPosition()
// Remove the observer
NotificationCenter.default.removeObserver(
self, name: .VLCMediaPlayerStateChanged, object: player)
}
}
} }
} }
@@ -200,25 +213,28 @@ class VlcPlayerView: ExpoView {
} }
} }
@objc func setMuted(_ muted: Bool) { // TODO
DispatchQueue.main.async { // @objc func setMuted(_ muted: Bool) {
self.mediaPlayer?.audio?.isMuted = muted // DispatchQueue.main.async {
} // self.mediaPlayer?.audio?.isMuted = muted
} // }
// }
@objc func setVolume(_ volume: Int) { // TODO
DispatchQueue.main.async { // @objc func setVolume(_ volume: Int) {
self.mediaPlayer?.audio?.volume = Int32(volume) // DispatchQueue.main.async {
} // self.mediaPlayer?.audio?.volume = Int32(volume)
} // }
// }
@objc func setVideoAspectRatio(_ ratio: String) { // TODO
DispatchQueue.main.async { // @objc func setVideoAspectRatio(_ ratio: String) {
ratio.withCString { cString in // DispatchQueue.main.async {
self.mediaPlayer?.videoAspectRatio = UnsafeMutablePointer(mutating: cString) // ratio.withCString { cString in
} // self.mediaPlayer?.videoAspectRatio = UnsafeMutablePointer(mutating: cString)
} // }
} // }
// }
@objc func setAudioTrack(_ trackIndex: Int) { @objc func setAudioTrack(_ trackIndex: Int) {
DispatchQueue.main.async { DispatchQueue.main.async {
@@ -373,88 +389,97 @@ class VlcPlayerView: ExpoView {
// } // }
// } // }
@objc func setSubtitleDelay(_ delay: Int) { // TODO
DispatchQueue.main.async { // @objc func setSubtitleDelay(_ delay: Int) {
self.mediaPlayer?.currentVideoSubTitleDelay = NSInteger(delay) // DispatchQueue.main.async {
} // self.mediaPlayer?.currentVideoSubTitleDelay = NSInteger(delay)
} // }
// }
@objc func setAudioDelay(_ delay: Int) { // TODO
DispatchQueue.main.async { // @objc func setAudioDelay(_ delay: Int) {
self.mediaPlayer?.currentAudioPlaybackDelay = NSInteger(delay) // DispatchQueue.main.async {
} // self.mediaPlayer?.currentAudioPlaybackDelay = NSInteger(delay)
} // }
// }
@objc func takeSnapshot(_ path: String, width: Int, height: Int) { // TODO
DispatchQueue.main.async { [weak self] in // @objc func takeSnapshot(_ path: String, width: Int, height: Int) {
guard let self = self else { return } // DispatchQueue.main.async { [weak self] in
self.mediaPlayer?.saveVideoSnapshot( // guard let self = self else { return }
at: path, withWidth: Int32(width), andHeight: Int32(height)) // self.mediaPlayer?.saveVideoSnapshot(
} // at: path, withWidth: Int32(width), andHeight: Int32(height))
} // }
// }
@objc func setVideoCropGeometry(_ geometry: String?) { // TODO
DispatchQueue.main.async { // @objc func setVideoCropGeometry(_ geometry: String?) {
if let geometry = geometry, !geometry.isEmpty { // DispatchQueue.main.async {
self.currentGeometryCString = geometry.cString(using: .utf8) // if let geometry = geometry, !geometry.isEmpty {
self.currentGeometryCString?.withUnsafeMutableBufferPointer { buffer in // self.currentGeometryCString = geometry.cString(using: .utf8)
self.mediaPlayer?.videoCropGeometry = buffer.baseAddress // self.currentGeometryCString?.withUnsafeMutableBufferPointer { buffer in
} // self.mediaPlayer?.videoCropGeometry = buffer.baseAddress
} else { // }
self.currentGeometryCString = nil // } else {
self.mediaPlayer?.videoCropGeometry = nil // self.currentGeometryCString = nil
} // self.mediaPlayer?.videoCropGeometry = nil
} // }
} // }
// }
@objc func getVideoCropGeometry() -> String? { // TODO
guard let cString = mediaPlayer?.videoCropGeometry else { // @objc func getVideoCropGeometry() -> String? {
return nil // guard let cString = mediaPlayer?.videoCropGeometry else {
} // return nil
return String(cString: cString) // }
} // return String(cString: cString)
// }
@objc func setRate(_ rate: Float) { // TODO
DispatchQueue.main.async { // @objc func setRate(_ rate: Float) {
self.mediaPlayer?.rate = rate // DispatchQueue.main.async {
} // self.mediaPlayer?.rate = rate
} // }
// }
@objc func nextChapter() { // TODO
DispatchQueue.main.async { // @objc func nextChapter() {
self.mediaPlayer?.nextChapter() // DispatchQueue.main.async {
} // self.mediaPlayer?.nextChapter()
} // }
// }
@objc func previousChapter() { // TODO
DispatchQueue.main.async { // @objc func previousChapter() {
self.mediaPlayer?.previousChapter() // DispatchQueue.main.async {
} // self.mediaPlayer?.previousChapter()
} // }
// }
@objc func getChapters() -> [[String: Any]]? { // TODO
guard let currentTitleIndex = mediaPlayer?.currentTitleIndex, // @objc func getChapters() -> [[String: Any]]? {
let chapters = mediaPlayer?.chapterDescriptions(ofTitle: currentTitleIndex) // guard let currentTitleIndex = mediaPlayer?.currentTitleIndex,
as? [[String: Any]] // let chapters = mediaPlayer?.chapterDescriptions(ofTitle: currentTitleIndex)
else { // as? [[String: Any]]
return nil // else {
} // return nil
// }
return chapters.compactMap { chapter in // return chapters.compactMap { chapter in
guard let name = chapter[VLCChapterDescriptionName] as? String, // guard let name = chapter[VLCChapterDescriptionName] as? String,
let timeOffset = chapter[VLCChapterDescriptionTimeOffset] as? NSNumber, // let timeOffset = chapter[VLCChapterDescriptionTimeOffset] as? NSNumber,
let duration = chapter[VLCChapterDescriptionDuration] as? NSNumber // let duration = chapter[VLCChapterDescriptionDuration] as? NSNumber
else { // else {
return nil // return nil
} // }
return [ // return [
"name": name, // "name": name,
"timeOffset": timeOffset.doubleValue, // "timeOffset": timeOffset.doubleValue,
"duration": duration.doubleValue, // "duration": duration.doubleValue,
] // ]
} // }
} // }
private var isStopping: Bool = false private var isStopping: Bool = false
@@ -641,16 +666,16 @@ extension VlcPlayerView: VLCMediaPlayerDelegate {
} }
extension VlcPlayerView: VLCMediaDelegate { extension VlcPlayerView: VLCMediaDelegate {
func mediaMetaDataDidChange(_ aMedia: VLCMedia) { // func mediaMetaDataDidChange(_ aMedia: VLCMedia) {
// Implement if needed // // Implement if needed
} // }
func mediaDidFinishParsing(_ aMedia: VLCMedia) { // func mediaDidFinishParsing(_ aMedia: VLCMedia) {
DispatchQueue.main.async { // DispatchQueue.main.async {
let duration = aMedia.length.intValue // let duration = aMedia.length.intValue
self.onVideoStateChange?(["type": "MediaParsed", "duration": duration]) // self.onVideoStateChange?(["type": "MediaParsed", "duration": duration])
} // }
} // }
} }
extension VLCMediaPlayerState { extension VLCMediaPlayerState {