diff --git a/Demo/Demo-iOS/Demo-iOS/Player/Metal/MPVMetalViewController.swift b/Demo/Demo-iOS/Demo-iOS/Player/Metal/MPVMetalViewController.swift index 746342d..c4c8870 100644 --- a/Demo/Demo-iOS/Demo-iOS/Player/Metal/MPVMetalViewController.swift +++ b/Demo/Demo-iOS/Demo-iOS/Player/Metal/MPVMetalViewController.swift @@ -89,6 +89,24 @@ final class MPVMetalViewController: UIViewController { let client = unsafeBitCast(ctx, to: MPVMetalViewController.self) client.readEvents() }, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) + + setupNotification() + } + + public func setupNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(enterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + } + + @objc public func enterBackground() { + // fix black screen issue when app enter foreground again + pause() + checkError(mpv_set_option_string(mpv, "vid", "no")) + } + + @objc public func enterForeground() { + checkError(mpv_set_option_string(mpv, "vid", "auto")) + play() } diff --git a/Demo/Demo-macOS/Demo-macOS.xcodeproj/project.pbxproj b/Demo/Demo-macOS/Demo-macOS.xcodeproj/project.pbxproj index 468f31b..ff4a381 100644 --- a/Demo/Demo-macOS/Demo-macOS.xcodeproj/project.pbxproj +++ b/Demo/Demo-macOS/Demo-macOS.xcodeproj/project.pbxproj @@ -363,6 +363,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mpvkit.Demo-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -389,6 +390,7 @@ "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.mpvkit.Demo-macOS"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/Demo/Demo-macOS/Demo-macOS/ContentView.swift b/Demo/Demo-macOS/Demo-macOS/ContentView.swift index 9ed62a3..d18c2a5 100644 --- a/Demo/Demo-macOS/Demo-macOS/ContentView.swift +++ b/Demo/Demo-macOS/Demo-macOS/ContentView.swift @@ -25,6 +25,7 @@ struct ContentView: View { } } } + .focusable() .overlay { HStack { VStack(alignment: .leading, spacing: 12) { @@ -49,6 +50,11 @@ struct ContentView: View { } label: { Text("subtitle").frame(maxWidth: .infinity) } + Button { + coordinator.play(URL(string: "https://framatube.org/static/streaming-playlists/hls/66fcff64-d8f9-49c1-8deb-011b115786de/73451ad8-d30e-4beb-aa92-3dba23ba07c8-720.m3u8")!) + } label: { + Text("hls").frame(maxWidth: .infinity) + } Button { coordinator.play(URL(string: "https://github.com/mpvkit/video-test/raw/master/resources/hdr.mkv")!) } label: { @@ -96,6 +102,18 @@ struct ContentView: View { .onHover { hover in showControlOverlay = hover } + .onKeyPress(action: { key in + debugPrint("key: \(key.characters)") + return .handled + }) + .onKeyPress(.leftArrow, action: { + coordinator.seek(relative: -10) + return .handled + }) + .onKeyPress(.rightArrow, action: { + coordinator.seek(relative: 10) + return .handled + }) .overlay(overlayView) .preferredColorScheme(.dark) .ignoresSafeArea() diff --git a/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalPlayerView.swift b/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalPlayerView.swift index daebf37..625be61 100644 --- a/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalPlayerView.swift +++ b/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalPlayerView.swift @@ -61,6 +61,10 @@ struct MPVMetalPlayerView: NSViewControllerRepresentable { self.pause = false } + func seek(relative time: TimeInterval) { + player?.seek(relative: time) + } + func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?) { guard let player else { return } self.onPropertyChange?(player, propertyName, data) diff --git a/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalViewController.swift b/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalViewController.swift index 0bb1720..e21e0bf 100644 --- a/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalViewController.swift +++ b/Demo/Demo-macOS/Demo-macOS/Player/Metal/MPVMetalViewController.swift @@ -1,5 +1,6 @@ import Foundation import AppKit +import CoreMedia import Libmpv // warning: metal API validation has been disabled to ignore crash when playing HDR videos. @@ -102,10 +103,10 @@ final class MPVMetalViewController: NSViewController { checkError(mpv_set_option_string(mpv, "gpu-context", "moltenvk")) checkError(mpv_set_option_string(mpv, "hwdec", "videotoolbox")) checkError(mpv_set_option_string(mpv, "ytdl", "no")) -// checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes")) // HDR passthrough -// checkError(mpv_set_option_string(mpv, "tone-mapping-visualize", "yes")) // only for debugging purposes -// checkError(mpv_set_option_string(mpv, "profile", "fast")) // can fix frame drop in poor device when play 4k - + // checkError(mpv_set_option_string(mpv, "target-colorspace-hint", "yes")) // HDR passthrough + // checkError(mpv_set_option_string(mpv, "tone-mapping-visualize", "yes")) // only for debugging purposes + // checkError(mpv_set_option_string(mpv, "profile", "fast")) // can fix frame drop in poor device when play 4k + checkError(mpv_initialize(mpv)) @@ -148,6 +149,10 @@ final class MPVMetalViewController: NSViewController { setFlag("pause", true) } + func seek(relative time: TimeInterval) { + command("seek", args: [String(time), "relative"]) + } + private func getDouble(_ name: String) -> Double { guard mpv != nil else { return 0.0 } var data = Double() @@ -194,7 +199,7 @@ final class MPVMetalViewController: NSViewController { } } - + private func makeCArgs(_ command: String, _ args: [String?]) -> [String?] { if !args.isEmpty, args.last == nil { diff --git a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVOGLView.swift b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVOGLView.swift index 0169007..2dcd50d 100644 --- a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVOGLView.swift +++ b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVOGLView.swift @@ -108,6 +108,10 @@ final class MPVOGLView: NSOpenGLView { command("loadfile", args: args) } + func seek(relative time: TimeInterval) { + command("seek", args: [String(time), "relative"]) + } + func getDouble(_ name: String) -> Double { guard mpv != nil else { return 0.0 } var data = Double() diff --git a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVPlayerView.swift b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVPlayerView.swift index 1097c44..f2b21c7 100644 --- a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVPlayerView.swift +++ b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVPlayerView.swift @@ -62,6 +62,10 @@ struct MPVPlayerView: NSViewControllerRepresentable { player?.loadFile(url) } + func seek(relative time: TimeInterval) { + player?.seek(relative: time) + } + func propertyChange(mpv: OpaquePointer, propertyName: String, data: Any?) { guard let player else { return } diff --git a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVViewController.swift b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVViewController.swift index 5239ef7..d2d5070 100644 --- a/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVViewController.swift +++ b/Demo/Demo-macOS/Demo-macOS/Player/OpenGL/MPVViewController.swift @@ -44,4 +44,8 @@ class MPVViewController: NSViewController { func pause() { self.glView.setFlag("pause", true) } + + func seek(relative time: TimeInterval) { + self.glView.seek(relative: time) + } } diff --git a/Demo/Demo-tvOS/Demo-tvOS/Player/Metal/MPVMetalViewController.swift b/Demo/Demo-tvOS/Demo-tvOS/Player/Metal/MPVMetalViewController.swift index 51124b9..ffde29a 100644 --- a/Demo/Demo-tvOS/Demo-tvOS/Player/Metal/MPVMetalViewController.swift +++ b/Demo/Demo-tvOS/Demo-tvOS/Player/Metal/MPVMetalViewController.swift @@ -87,8 +87,25 @@ final class MPVMetalViewController: UIViewController { let client = unsafeBitCast(ctx, to: MPVMetalViewController.self) client.readEvents() }, UnsafeMutableRawPointer(Unmanaged.passUnretained(self).toOpaque())) + + setupNotification() + } + + public func setupNotification() { + NotificationCenter.default.addObserver(self, selector: #selector(enterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(enterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) } + @objc public func enterBackground() { + // fix black screen issue when app enter foreground again + pause() + checkError(mpv_set_option_string(mpv, "vid", "no")) + } + + @objc public func enterForeground() { + checkError(mpv_set_option_string(mpv, "vid", "auto")) + play() + } func loadFile( _ url: URL diff --git a/Sources/BuildScripts/XCFrameworkBuild/main.swift b/Sources/BuildScripts/XCFrameworkBuild/main.swift index c34efc3..a6518d4 100644 --- a/Sources/BuildScripts/XCFrameworkBuild/main.swift +++ b/Sources/BuildScripts/XCFrameworkBuild/main.swift @@ -50,7 +50,7 @@ enum Library: String, CaseIterable { case .libmpv: return "v0.40.0" case .FFmpeg: - return "n7.1.1" + return "n8.0" case .openssl: return "3.2.0" case .gnutls: @@ -654,7 +654,7 @@ private class BuildFFMPEG: BaseBuild { "--disable-doc", "--disable-htmlpages", "--disable-manpages", "--disable-podpages", "--disable-txtpages", // Component options: "--enable-avcodec", "--enable-avformat", "--enable-avutil", "--enable-network", "--enable-swresample", "--enable-swscale", - "--disable-devices", "--disable-outdevs", "--disable-indevs", "--disable-postproc", + "--disable-devices", "--disable-outdevs", "--disable-indevs", // ,"--disable-pthreads" // ,"--disable-w32threads" // ,"--disable-os2threads" diff --git a/Sources/BuildScripts/patch/FFmpeg/0001-hls-seek-patch-1.patch b/Sources/BuildScripts/patch/FFmpeg/0001-hls-seek-patch-1.patch deleted file mode 100644 index 872056d..0000000 --- a/Sources/BuildScripts/patch/FFmpeg/0001-hls-seek-patch-1.patch +++ /dev/null @@ -1,39 +0,0 @@ -From 0c14ac8d5f19f22f31ae9505e3db5eb417e8d6e0 Mon Sep 17 00:00:00 2001 -From: llyyr -Date: Sun, 27 Oct 2024 06:52:42 +0530 -Subject: [PATCH 1/2] avformat/hls: always return keyframe if not - AVSEEK_FLAG_ANY - -Co-Authored-by: vectronic ---- - libavformat/hls.c | 8 +++++--- - 1 file changed, 5 insertions(+), 3 deletions(-) - -diff --git a/libavformat/hls.c b/libavformat/hls.c -index 62473a15ddb5..4d02faa9e49a 100644 ---- a/libavformat/hls.c -+++ b/libavformat/hls.c -@@ -2350,8 +2350,10 @@ static int hls_read_packet(AVFormatContext *s, AVPacket *pkt) - ts_diff = av_rescale_rnd(pls->pkt->dts, AV_TIME_BASE, - tb.den, AV_ROUND_DOWN) - - pls->seek_timestamp; -- if (ts_diff >= 0 && (pls->seek_flags & AVSEEK_FLAG_ANY || -- pls->pkt->flags & AV_PKT_FLAG_KEY)) { -+ /* If AVSEEK_FLAG_ANY, keep reading until ts_diff >= 0, -+ * otherwise return the first keyframe encountered */ -+ if ((ts_diff >= 0 && (pls->seek_flags & AVSEEK_FLAG_ANY)) || -+ (!(pls->seek_flags & AVSEEK_FLAG_ANY) && (pls->pkt->flags & AV_PKT_FLAG_KEY))) { - pls->seek_timestamp = AV_NOPTS_VALUE; - break; - } -@@ -2502,7 +2504,7 @@ static int hls_read_seek(AVFormatContext *s, int stream_index, - pb->eof_reached = 0; - /* Clear any buffered data */ - pb->buf_end = pb->buf_ptr = pb->buffer; -- /* Reset the pos, to let the mpegts demuxer know we've seeked. */ -+ /* Reset the pos, to let the mpegts/mov demuxer know we've seeked. */ - pb->pos = 0; - /* Flush the packet queue of the subdemuxer. */ - ff_read_frame_flush(pls->ctx); --- -2.47.0 \ No newline at end of file diff --git a/Sources/BuildScripts/patch/FFmpeg/0002-hls-seek-patch-2.patch b/Sources/BuildScripts/patch/FFmpeg/0002-hls-seek-patch-2.patch deleted file mode 100644 index 157df14..0000000 --- a/Sources/BuildScripts/patch/FFmpeg/0002-hls-seek-patch-2.patch +++ /dev/null @@ -1,78 +0,0 @@ -From 07045b8245cfc5822005c716db8a84b710579787 Mon Sep 17 00:00:00 2001 -From: llyyr -Date: Sun, 27 Oct 2024 06:44:51 +0530 -Subject: [PATCH 2/2] avformat/mov: handle stream position resets - -If the stream position has been reset, clear fragment index, the index -entries and the current sample and search for the next root. - -Co-Authored-by: vectronic ---- - libavformat/mov.c | 37 +++++++++++++++++++++++++++++++++---- - 1 file changed, 33 insertions(+), 4 deletions(-) - -diff --git a/libavformat/mov.c b/libavformat/mov.c -index 8c3329b81596..78540bec1d11 100644 ---- a/libavformat/mov.c -+++ b/libavformat/mov.c -@@ -10612,15 +10612,15 @@ static int mov_switch_root(AVFormatContext *s, int64_t target, int index) - - if (index >= 0 && index < mov->frag_index.nb_items) - target = mov->frag_index.item[index].moof_offset; -- if (avio_seek(s->pb, target, SEEK_SET) != target) { -+ if (target >= 0 && avio_seek(s->pb, target, SEEK_SET) != target) { - av_log(mov->fc, AV_LOG_ERROR, "root atom offset 0x%"PRIx64": partial file\n", target); - return AVERROR_INVALIDDATA; - } - - mov->next_root_atom = 0; -- if (index < 0 || index >= mov->frag_index.nb_items) -+ if ((index < 0 && target >= 0) || index >= mov->frag_index.nb_items) - index = search_frag_moof_offset(&mov->frag_index, target); -- if (index < mov->frag_index.nb_items && -+ if (index >= 0 && index < mov->frag_index.nb_items && - mov->frag_index.item[index].moof_offset == target) { - if (index + 1 < mov->frag_index.nb_items) - mov->next_root_atom = mov->frag_index.item[index + 1].moof_offset; -@@ -10751,9 +10751,38 @@ static int mov_read_packet(AVFormatContext *s, AVPacket *pkt) - AVIndexEntry *sample; - AVStream *st = NULL; - int64_t current_index; -- int ret; -+ int ret, i; - mov->fc = s; - retry: -+ if (s->pb->pos == 0) { -+ // Discard current fragment index -+ if (mov->frag_index.allocated_size > 0) { -+ av_freep(&mov->frag_index.item); -+ mov->frag_index.nb_items = 0; -+ mov->frag_index.allocated_size = 0; -+ mov->frag_index.current = -1; -+ mov->frag_index.complete = 0; -+ } -+ -+ for (i = 0; i < s->nb_streams; i++) { -+ AVStream *avst = s->streams[i]; -+ FFStream *sti = ffstream(avst); -+ MOVStreamContext *msc = avst->priv_data; -+ -+ // Clear current sample -+ mov_current_sample_set(msc, 0); -+ -+ // Discard current index entries -+ if (sti->index_entries_allocated_size > 0) { -+ av_freep(&sti->index_entries); -+ sti->index_entries_allocated_size = 0; -+ sti->nb_index_entries = 0; -+ } -+ } -+ -+ if ((ret = mov_switch_root(s, -1, -1)) < 0) -+ return ret; -+ } - sample = mov_find_next_sample(s, &st); - if (!sample || (mov->next_root_atom && sample->pos > mov->next_root_atom)) { - if (!mov->next_root_atom) --- -2.47.0 \ No newline at end of file diff --git a/Sources/BuildScripts/patch/libmpv/0003-fix-ffmpeg-n8.0.patch b/Sources/BuildScripts/patch/libmpv/0003-fix-ffmpeg-n8.0.patch new file mode 100644 index 0000000..bbc3e47 --- /dev/null +++ b/Sources/BuildScripts/patch/libmpv/0003-fix-ffmpeg-n8.0.patch @@ -0,0 +1,35 @@ +From 26b29fba02a2782f68e2906f837d21201fc6f1b9 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Kacper=20Michaj=C5=82ow?= +Date: Fri, 28 Mar 2025 19:12:01 +0100 +Subject: [PATCH] demux_mkv: fix compilation after deprecated definitions + removal + +See: https://github.com/FFmpeg/FFmpeg/commit/822432769868da325ba03774df1084aa78b9a5a0 +--- + demux/demux_mkv.c | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) + +diff --git a/demux/demux_mkv.c b/demux/demux_mkv.c +index 135edcc23d82b..cc7ce3e98f4f6 100644 +--- a/demux/demux_mkv.c ++++ b/demux/demux_mkv.c +@@ -2200,16 +2200,16 @@ static int demux_mkv_open_sub(demuxer_t *demuxer, mkv_track_t *track) + // [0x30..0x37] are component tags utilized for + // non-mobile captioning service ("profile A"). + if (component_tag >= 0x30 && component_tag <= 0x37) +- lav->profile = FF_PROFILE_ARIB_PROFILE_A; ++ lav->profile = AV_PROFILE_ARIB_PROFILE_A; + break; + case 0x0012: + // component tag 0x87 signifies a mobile/partial reception + // (1seg) captioning service ("profile C"). + if (component_tag == 0x87) +- lav->profile = FF_PROFILE_ARIB_PROFILE_C; ++ lav->profile = AV_PROFILE_ARIB_PROFILE_C; + break; + } +- if (lav->profile == FF_PROFILE_UNKNOWN) ++ if (lav->profile == AV_PROFILE_UNKNOWN) + MP_WARN(demuxer, "ARIB caption profile %02x / %04x not supported.\n", + component_tag, data_component_id); + }