Add iOS Picture in Picture via VLCKit 4 upgrade.
This commit is contained in:
parent
796edf36db
commit
a38824db27
9 changed files with 337 additions and 271 deletions
|
|
@ -1,4 +1,4 @@
|
|||
# VMC iOS (SwiftUI + MobileVLCKit)
|
||||
# VMC iOS (SwiftUI + VLCKit 4)
|
||||
|
||||
Native iPhone/iPad app voor Vonas Media Cluster.
|
||||
|
||||
|
|
@ -9,17 +9,25 @@ cd ~/Code/mediacluster/apps/ios
|
|||
open VMC.xcodeproj
|
||||
```
|
||||
|
||||
Eerste keer (of na schone sync) VLCKit zetten:
|
||||
Eerste keer (of na schone sync) **VLCKit 4** zetten (PiP):
|
||||
|
||||
```bash
|
||||
bash scripts/fetch-vlckit.sh # download ~250MB (eenmalig)
|
||||
bash scripts/link-vlckit.sh # kopieert naar Vendor/ + linkt in Xcode
|
||||
bash scripts/fetch-vlckit.sh # download ~500MB VLCKit 4.0.0a19
|
||||
bash scripts/link-vlckit.sh # → Vendor/VLCKit.xcframework
|
||||
```
|
||||
|
||||
> **Let op:** MobileVLCKit 3.x heeft **geen** Apple Picture in Picture. VMC iOS 0.5.3+ vereist VLCKit 4.
|
||||
|
||||
In Xcode:
|
||||
1. **Signing & Capabilities** → jouw Team
|
||||
1. **Signing & Capabilities** → Team + Background Modes → *Audio, AirPlay, and Picture in Picture*
|
||||
2. iPhone selecteren → ▶ Run
|
||||
|
||||
## Picture in Picture
|
||||
|
||||
- Systeem-PiP via VLCKit (`VLCPictureInPictureDrawable`)
|
||||
- PiP-knop in de speler + automatisch bij app naar achtergrond tijdens afspelen
|
||||
- ClearKey/DASH blijft via lokale `ClearKeyDashProxy` → plain DASH in VLC
|
||||
|
||||
---
|
||||
|
||||
## Apple TV (tvOS)
|
||||
|
|
@ -35,17 +43,17 @@ open VMCTV.xcodeproj
|
|||
|
||||
- Bundle ID: `nl.vonas.mediacluster.tvos`
|
||||
- Team: `X25NYX3K49`
|
||||
- Playback: **TVVLCKit** (zelfde VLC-engine als iPhone) — MKV werkt
|
||||
- Playback: **TVVLCKit** (VLC 3.x engine) — PiP is iPhone/iPad-only
|
||||
- Details: `VMCTV/README.md`
|
||||
|
||||
## Let op
|
||||
|
||||
`Vendor/MobileVLCKit.xcframework` en `Vendor/TVVLCKit.xcframework` staan niet in git (te groot). Altijd op de Mac via de scripts installeren.
|
||||
`Vendor/VLCKit.xcframework` en `Vendor/TVVLCKit.xcframework` staan niet in git (te groot). Altijd op de Mac via de scripts installeren.
|
||||
|
||||
## Live TV / Events / ClearKey (0.5.2+)
|
||||
|
||||
- `streamId` is een **String** (`"123"` of `"custom:uuid"`); path-segmenten worden URL-geëncodeerd (inclusief `:`).
|
||||
- Play-response parseert `format`, `drm` (ClearKey kids/keys) en `watchSessionId` voor live-lijsten én events.
|
||||
- Heartbeat: `POST /api/v1/client/watching/heartbeat` ~elke 25s bij custom live + events.
|
||||
- **ClearKey/DASH:** lokale in-app proxy (`ClearKeyDashProxy`) haalt MPD/segmenten op (dynamic live: BaseURL/Location/query/Range), decrypt CENC/CBCS (`sinf`→`free`), en speelt plain DASH via MobileVLCKit. Xtream HLS/TS speelt direct zonder proxy.
|
||||
- **ClearKey/DASH:** lokale in-app proxy (`ClearKeyDashProxy`) haalt MPD/segmenten op (dynamic live: BaseURL/Location/query/Range), decrypt CENC/CBCS (`sinf`→`free`), en speelt plain DASH via VLCKit. Xtream HLS/TS speelt direct zonder proxy.
|
||||
- Events-tab verschijnt als `/me` → `events.enabled` true is.
|
||||
|
|
|
|||
|
|
@ -36,8 +36,8 @@
|
|||
B8D4E1F2A3C54B6D9E0F1A2B /* DevicesScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = C1D2E3F4A5B647C8D9E0F1A3 /* DevicesScreen.swift */; };
|
||||
B7C2E91A4F8D4A1E9C3B6D20 /* QualityBadges.swift in Sources */ = {isa = PBXBuildFile; fileRef = D4A1F82E6B9C4E7A8F1D3C50 /* QualityBadges.swift */; };
|
||||
468E05CB91FC4A1997A2F386 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6FA95D6A03BF48EA97553486 /* Assets.xcassets */; };
|
||||
DC8D7BF328134E1E8D7E3D76 /* MobileVLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE257D7C209E4803903206A8 /* MobileVLCKit.xcframework */; };
|
||||
E9BB0F1F350A442FB6440AE5 /* MobileVLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BE257D7C209E4803903206A8 /* MobileVLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
DC8D7BF328134E1E8D7E3D76 /* VLCKit.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = BE257D7C209E4803903206A8 /* VLCKit.xcframework */; };
|
||||
E9BB0F1F350A442FB6440AE5 /* VLCKit.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = BE257D7C209E4803903206A8 /* VLCKit.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
|
|
@ -72,7 +72,7 @@
|
|||
D4A1F82E6B9C4E7A8F1D3C50 /* QualityBadges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QualityBadges.swift; sourceTree = "<group>"; };
|
||||
6FA95D6A03BF48EA97553486 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||
A22966A1CCBE44999F75FCA2 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
BE257D7C209E4803903206A8 /* MobileVLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Vendor/MobileVLCKit.xcframework; sourceTree = "<group>"; };
|
||||
BE257D7C209E4803903206A8 /* VLCKit.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = VLCKit.xcframework; path = Vendor/VLCKit.xcframework; sourceTree = "<group>"; };
|
||||
E3BC8A7BDE75425E84A174DE /* VMC.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = VMC.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
|
|
@ -83,7 +83,7 @@
|
|||
dstPath = "";
|
||||
dstSubfolderSpec = 10;
|
||||
files = (
|
||||
E9BB0F1F350A442FB6440AE5 /* MobileVLCKit.xcframework in Embed Frameworks */,
|
||||
E9BB0F1F350A442FB6440AE5 /* VLCKit.xcframework in Embed Frameworks */,
|
||||
);
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
|
@ -95,7 +95,7 @@
|
|||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
DC8D7BF328134E1E8D7E3D76 /* MobileVLCKit.xcframework in Frameworks */,
|
||||
DC8D7BF328134E1E8D7E3D76 /* VLCKit.xcframework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
7200A95B2D6F482FA1FDA1A1 /* VMC */,
|
||||
BE257D7C209E4803903206A8 /* MobileVLCKit.xcframework */,
|
||||
BE257D7C209E4803903206A8 /* VLCKit.xcframework */,
|
||||
AA621A418F3940A281D959B4 /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
|
|
@ -337,7 +337,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -352,7 +352,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.2;
|
||||
MARKETING_VERSION = 0.5.3;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -370,7 +370,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 14;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.2;
|
||||
MARKETING_VERSION = 0.5.3;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -548,7 +548,3 @@ struct ApiErrorBody: Decodable {
|
|||
let error: ErrorObj?
|
||||
}
|
||||
|
||||
struct PlayerTrack: Identifiable, Hashable {
|
||||
let id: Int32
|
||||
let name: String
|
||||
}
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@
|
|||
</array>
|
||||
<key>NSPhotoLibraryUsageDescription</key>
|
||||
<string>VMC gebruikt je fotobibliotheek om een profielfoto te kiezen.</string>
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>audio</string>
|
||||
</array>
|
||||
<key>UILaunchScreen</key>
|
||||
<dict>
|
||||
<key>UIColorName</key>
|
||||
|
|
|
|||
|
|
@ -159,6 +159,20 @@ struct PlayerScreen: View {
|
|||
.foregroundStyle(.white.opacity(0.7))
|
||||
}
|
||||
Spacer()
|
||||
if player.pipPossible {
|
||||
Button {
|
||||
player.togglePictureInPicture()
|
||||
scheduleHide()
|
||||
} label: {
|
||||
Image(systemName: player.pipActive
|
||||
? "pip.exit"
|
||||
: "pip.enter")
|
||||
.font(.title3)
|
||||
.foregroundStyle(.white)
|
||||
.padding(12)
|
||||
.background(.black.opacity(0.4), in: Circle())
|
||||
}
|
||||
}
|
||||
if !request.isLive {
|
||||
Button {
|
||||
showTracks = true
|
||||
|
|
|
|||
|
|
@ -1,7 +1,14 @@
|
|||
import AVKit
|
||||
import Combine
|
||||
import MobileVLCKit
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import VLCKit
|
||||
|
||||
struct PlayerTrack: Identifiable, Hashable {
|
||||
/// VLCKit4 trackId, of `"__off__"` voor ondertitels uit.
|
||||
let id: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDelegate, VLCMediaDelegate {
|
||||
@Published var isPlaying = false
|
||||
|
|
@ -13,10 +20,16 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
@Published var errorMessage: String?
|
||||
@Published var subtitleTracks: [PlayerTrack] = []
|
||||
@Published var audioTracks: [PlayerTrack] = []
|
||||
@Published var currentSubtitleId: Int32 = -1
|
||||
@Published var currentAudioId: Int32 = -1
|
||||
@Published var currentSubtitleId: String = PlayerTrack.offId
|
||||
@Published var currentAudioId: String = ""
|
||||
@Published var pipPossible = false
|
||||
@Published var pipActive = false
|
||||
|
||||
let mediaPlayer = VLCMediaPlayer()
|
||||
static let offSubtitleId = PlayerTrack.offId
|
||||
|
||||
private let library: VLCLibrary
|
||||
let mediaPlayer: VLCMediaPlayer
|
||||
private let drawableHost = VLCPipDrawableHost()
|
||||
private var lengthMs: Int32 = 0
|
||||
private var tick: AnyCancellable?
|
||||
private var clearKeyProxyActive = false
|
||||
|
|
@ -25,14 +38,43 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
private var liveStartedAt: Date?
|
||||
private var liveRetryCount = 0
|
||||
private var everPlayed = false
|
||||
private var pipWindow: VLCPictureInPictureWindowControlling?
|
||||
private var sceneObserver: NSObjectProtocol?
|
||||
|
||||
override init() {
|
||||
// Eigen library-instance zodat opties/PiP-vout stabiel blijven.
|
||||
library = VLCLibrary(options: [])
|
||||
mediaPlayer = VLCMediaPlayer(library: library)
|
||||
super.init()
|
||||
mediaPlayer.delegate = self
|
||||
drawableHost.controller = self
|
||||
mediaPlayer.drawable = drawableHost
|
||||
configureAudioSession()
|
||||
observeSceneForAutoPip()
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let sceneObserver {
|
||||
NotificationCenter.default.removeObserver(sceneObserver)
|
||||
}
|
||||
}
|
||||
|
||||
func attach(to view: UIView) {
|
||||
mediaPlayer.drawable = view
|
||||
drawableHost.attach(to: view)
|
||||
mediaPlayer.drawable = drawableHost
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
pipWindow?.startPictureInPicture()
|
||||
}
|
||||
|
||||
func stopPictureInPicture() {
|
||||
pipWindow?.stopPictureInPicture()
|
||||
}
|
||||
|
||||
func togglePictureInPicture() {
|
||||
if pipActive { stopPictureInPicture() }
|
||||
else { startPictureInPicture() }
|
||||
}
|
||||
|
||||
func play(url: URL, startMs: Int64 = 0, subtitleFileURL: URL? = nil) {
|
||||
|
|
@ -48,13 +90,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
media.addOption(":avcodec-hw=any")
|
||||
media.addOption(":sout-mux-caching=1500")
|
||||
media.addOption(":sub-track=0")
|
||||
// VLC: hoger = kleiner (hoogte ≈ scherm/N). Default ~16 is te groot op iPhone.
|
||||
media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)")
|
||||
media.addOption(":freetype-bold=1")
|
||||
mediaPlayer.media = media
|
||||
// Async parse zodat tracksInformation language/title beschikbaar wordt.
|
||||
// MobileVLCKit (deze binary) exposeert parseWithOptions: niet netjes naar Swift.
|
||||
media.parse()
|
||||
_ = media.parseWithOptions(VLCMediaParsingOptions(rawValue: 0x02 | 0x08)) // network + fetchLocal
|
||||
mediaPlayer.play()
|
||||
applySubtitleStyle()
|
||||
if startMs > 2000 {
|
||||
|
|
@ -93,8 +132,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
|
||||
if !clearKeys.isEmpty {
|
||||
do {
|
||||
// MobileVLCKit kan CENC/ClearKey niet zelf; lokale proxy decrypt segmenten.
|
||||
// start() wacht tot listener + MPD-warmup klaar zijn.
|
||||
// VLCKit speelt geen CENC/ClearKey zelf; lokale proxy decrypt segmenten.
|
||||
playURL = try ClearKeyDashProxy.shared.start(manifestURL: url, keys: clearKeys)
|
||||
playFormat = "dash"
|
||||
clearKeyProxyActive = true
|
||||
|
|
@ -125,7 +163,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
media.delegate = self
|
||||
Self.applyLiveOptions(to: media, url: url, format: format)
|
||||
mediaPlayer.media = media
|
||||
// Geen parse() op live/DASH — blokkeert/stopt soms meteen op dynamic MPD.
|
||||
mediaPlayer.play()
|
||||
applySubtitleStyle()
|
||||
}
|
||||
|
|
@ -147,7 +184,6 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
return "ClearKey-stream mislukt: \(msg)"
|
||||
}
|
||||
|
||||
/// Na vroege Gestopt/error: één keer opnieuw afspelen (proxy blijft actief).
|
||||
private func retryLiveIfNeeded() {
|
||||
guard clearKeyProxyActive, !everPlayed, liveRetryCount < 2 else { return }
|
||||
guard let url = livePlayURL else { return }
|
||||
|
|
@ -190,14 +226,17 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
func togglePlay() {
|
||||
if mediaPlayer.isPlaying { mediaPlayer.pause() }
|
||||
else { mediaPlayer.play() }
|
||||
invalidatePipPlaybackState()
|
||||
}
|
||||
|
||||
func seek(toMs: Int64) {
|
||||
mediaPlayer.time = VLCTime(number: NSNumber(value: toMs))
|
||||
invalidatePipPlaybackState()
|
||||
}
|
||||
|
||||
func seek(ratio: Float) {
|
||||
mediaPlayer.position = ratio
|
||||
mediaPlayer.position = Double(ratio)
|
||||
invalidatePipPlaybackState()
|
||||
}
|
||||
|
||||
func currentPositionMs() -> Int64 { Int64(mediaPlayer.time.intValue) }
|
||||
|
|
@ -207,25 +246,33 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
return Int64(max(v, 0))
|
||||
}
|
||||
|
||||
func selectSubtitle(id: Int32) {
|
||||
mediaPlayer.currentVideoSubTitleIndex = id
|
||||
currentSubtitleId = id
|
||||
func selectSubtitle(id: String) {
|
||||
if id == PlayerTrack.offId {
|
||||
mediaPlayer.deselectAllTextTracks()
|
||||
currentSubtitleId = PlayerTrack.offId
|
||||
} else if let track = mediaPlayer.textTracks.first(where: { $0.trackId == id }) {
|
||||
track.isSelectedExclusively = true
|
||||
currentSubtitleId = id
|
||||
}
|
||||
invalidatePipPlaybackState()
|
||||
}
|
||||
|
||||
func selectAudio(id: Int32) {
|
||||
mediaPlayer.currentAudioTrackIndex = id
|
||||
currentAudioId = id
|
||||
func selectAudio(id: String) {
|
||||
if let track = mediaPlayer.audioTracks.first(where: { $0.trackId == id }) {
|
||||
track.isSelectedExclusively = true
|
||||
currentAudioId = id
|
||||
}
|
||||
invalidatePipPlaybackState()
|
||||
}
|
||||
|
||||
func addExternalSubtitle(url: URL) {
|
||||
mediaPlayer.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||
_ = mediaPlayer.addPlaybackSlave(url, type: .subtitle, enforce: true)
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { [weak self] in
|
||||
self?.refreshTracks()
|
||||
self?.applySubtitleStyle()
|
||||
}
|
||||
}
|
||||
|
||||
/// VLC relative fontsize: groter getal = kleinere tekst (~1/N van schermhoogte).
|
||||
private static let subtitleRelFontSize = 32
|
||||
|
||||
private func applySubtitleStyle() {
|
||||
|
|
@ -237,101 +284,39 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
|
||||
func refreshTracks() {
|
||||
let meta = trackMetaById()
|
||||
var textOrderHints: [String?] = []
|
||||
var audioOrderHints: [String?] = []
|
||||
for info in (mediaPlayer.media?.tracksInformation as? [Any]) ?? [] {
|
||||
guard let dict = info as? [AnyHashable: Any] else { continue }
|
||||
let type = ((dict[VLCMediaTracksInformationType] as? String)
|
||||
?? (dict["type"] as? String)
|
||||
?? "").lowercased()
|
||||
let lang = (dict[VLCMediaTracksInformationLanguage] as? String)
|
||||
?? (dict["language"] as? String)
|
||||
let desc = (dict[VLCMediaTracksInformationDescription] as? String)
|
||||
?? (dict["description"] as? String)
|
||||
let hint = lang ?? desc
|
||||
if type.contains("text") {
|
||||
textOrderHints.append(hint)
|
||||
} else if type.contains("audio") {
|
||||
audioOrderHints.append(hint)
|
||||
}
|
||||
var subs: [PlayerTrack] = [PlayerTrack(id: PlayerTrack.offId, name: "Uit")]
|
||||
for (index, track) in mediaPlayer.textTracks.enumerated() {
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: track.trackName,
|
||||
fallbackIndex: index + 1,
|
||||
kind: .subtitle,
|
||||
languageHint: track.language,
|
||||
descriptionHint: track.trackDescription
|
||||
)
|
||||
subs.append(PlayerTrack(id: track.trackId, name: label))
|
||||
if track.isSelected { currentSubtitleId = track.trackId }
|
||||
}
|
||||
|
||||
var subs: [PlayerTrack] = [PlayerTrack(id: -1, name: "Uit")]
|
||||
var textOrd = 0
|
||||
if let names = mediaPlayer.videoSubTitlesNames as? [Any],
|
||||
let indexes = mediaPlayer.videoSubTitlesIndexes as? [Any] {
|
||||
for (i, raw) in indexes.enumerated() {
|
||||
let idx = (raw as? NSNumber)?.int32Value ?? Int32(truncating: raw as? NSNumber ?? -1)
|
||||
if idx < 0 { continue }
|
||||
let rawName: String = {
|
||||
if i < names.count, let n = names[i] as? String, !n.isEmpty { return n }
|
||||
return ""
|
||||
}()
|
||||
let fromId = meta[idx]
|
||||
let fromOrder = textOrd < textOrderHints.count ? textOrderHints[textOrd] : nil
|
||||
textOrd += 1
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: rawName,
|
||||
fallbackIndex: subs.count,
|
||||
kind: .subtitle,
|
||||
languageHint: fromId?.language ?? fromOrder,
|
||||
descriptionHint: fromId?.description
|
||||
)
|
||||
subs.append(PlayerTrack(id: idx, name: label))
|
||||
}
|
||||
if !mediaPlayer.textTracks.contains(where: \.isSelected) {
|
||||
currentSubtitleId = PlayerTrack.offId
|
||||
}
|
||||
subtitleTracks = subs
|
||||
currentSubtitleId = mediaPlayer.currentVideoSubTitleIndex
|
||||
|
||||
var audios: [PlayerTrack] = []
|
||||
var audioOrd = 0
|
||||
if let names = mediaPlayer.audioTrackNames as? [Any],
|
||||
let indexes = mediaPlayer.audioTrackIndexes as? [Any] {
|
||||
for (i, raw) in indexes.enumerated() {
|
||||
let idx = (raw as? NSNumber)?.int32Value ?? -1
|
||||
if idx < 0 { continue }
|
||||
let rawName: String = {
|
||||
if i < names.count, let n = names[i] as? String, !n.isEmpty { return n }
|
||||
return ""
|
||||
}()
|
||||
let fromId = meta[idx]
|
||||
let fromOrder = audioOrd < audioOrderHints.count ? audioOrderHints[audioOrd] : nil
|
||||
audioOrd += 1
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: rawName,
|
||||
fallbackIndex: audios.count + 1,
|
||||
kind: .audio,
|
||||
languageHint: fromId?.language ?? fromOrder,
|
||||
descriptionHint: fromId?.description
|
||||
)
|
||||
audios.append(PlayerTrack(id: idx, name: label))
|
||||
}
|
||||
for (index, track) in mediaPlayer.audioTracks.enumerated() {
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: track.trackName,
|
||||
fallbackIndex: index + 1,
|
||||
kind: .audio,
|
||||
languageHint: track.language,
|
||||
descriptionHint: track.trackDescription
|
||||
)
|
||||
audios.append(PlayerTrack(id: track.trackId, name: label))
|
||||
if track.isSelected { currentAudioId = track.trackId }
|
||||
}
|
||||
if currentAudioId.isEmpty, let first = audios.first {
|
||||
currentAudioId = first.id
|
||||
}
|
||||
audioTracks = audios
|
||||
currentAudioId = mediaPlayer.currentAudioTrackIndex
|
||||
}
|
||||
|
||||
private struct TrackMeta {
|
||||
var language: String?
|
||||
var description: String?
|
||||
}
|
||||
|
||||
private func trackMetaById() -> [Int32: TrackMeta] {
|
||||
guard let media = mediaPlayer.media else { return [:] }
|
||||
var out: [Int32: TrackMeta] = [:]
|
||||
for info in (media.tracksInformation as? [Any]) ?? [] {
|
||||
guard let dict = info as? [AnyHashable: Any] else { continue }
|
||||
let tid = (dict[VLCMediaTracksInformationId] as? NSNumber)?.int32Value
|
||||
?? (dict["id"] as? NSNumber)?.int32Value
|
||||
guard let tid else { continue }
|
||||
let lang = (dict[VLCMediaTracksInformationLanguage] as? String)
|
||||
?? (dict["language"] as? String)
|
||||
let desc = (dict[VLCMediaTracksInformationDescription] as? String)
|
||||
?? (dict["description"] as? String)
|
||||
out[tid] = TrackMeta(language: lang, description: desc)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
private enum TrackKind { case subtitle, audio }
|
||||
|
|
@ -447,6 +432,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
func stop() {
|
||||
tick?.cancel()
|
||||
tick = nil
|
||||
stopPictureInPicture()
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.media = nil
|
||||
livePlayURL = nil
|
||||
|
|
@ -460,11 +446,14 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
}
|
||||
|
||||
func mediaPlayerStateChanged(_ aNotification: Notification) {
|
||||
// MARK: - VLCMediaPlayerDelegate (VLCKit 4)
|
||||
|
||||
func mediaPlayerStateChanged(_ newState: VLCMediaPlayerState) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.isPlaying = self.mediaPlayer.isPlaying
|
||||
switch self.mediaPlayer.state {
|
||||
self.invalidatePipPlaybackState()
|
||||
switch newState {
|
||||
case .buffering: self.stateText = "Bufferen…"
|
||||
case .opening: self.stateText = "Openen…"
|
||||
case .playing:
|
||||
|
|
@ -475,15 +464,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
self.refreshTracks()
|
||||
self.applySubtitleStyle()
|
||||
case .paused: self.stateText = "Pauze"
|
||||
case .stopped:
|
||||
// Vroege "Gestopt" bij ClearKey is vaak tijdelijk (init/segment race) — niet meteen opgeven.
|
||||
case .stopped, .stopping:
|
||||
if self.clearKeyProxyActive && !self.everPlayed {
|
||||
self.stateText = "Verbinden…"
|
||||
self.retryLiveIfNeeded()
|
||||
} else {
|
||||
self.stateText = "Gestopt"
|
||||
}
|
||||
case .ended: self.stateText = "Einde"
|
||||
case .error:
|
||||
if self.clearKeyProxyActive && !self.everPlayed && self.liveRetryCount < 2 {
|
||||
self.stateText = "Opnieuw…"
|
||||
|
|
@ -495,13 +482,25 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
: "Kon dit bestand niet afspelen"
|
||||
self.stateText = "Fout"
|
||||
}
|
||||
case .esAdded:
|
||||
self.refreshTracks()
|
||||
@unknown default: break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mediaPlayerTrackAdded(_ trackId: String, with type: VLCMedia.TrackType) {
|
||||
DispatchQueue.main.async { [weak self] in self?.refreshTracks() }
|
||||
}
|
||||
|
||||
func mediaPlayerTrackUpdated(_ trackId: String, with type: VLCMedia.TrackType) {
|
||||
DispatchQueue.main.async { [weak self] in self?.refreshTracks() }
|
||||
}
|
||||
|
||||
func mediaPlayerLengthChanged(_ length: Int64) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.invalidatePipPlaybackState()
|
||||
}
|
||||
}
|
||||
|
||||
func mediaDidFinishParsing(_ aMedia: VLCMedia) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.refreshTracks()
|
||||
|
|
@ -521,7 +520,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
let t = mediaPlayer.time.intValue
|
||||
let l = mediaPlayer.media?.length.intValue ?? 0
|
||||
if l > 0 { lengthMs = l }
|
||||
position = mediaPlayer.position
|
||||
position = Float(mediaPlayer.position)
|
||||
timeText = Self.format(ms: t)
|
||||
lengthText = Self.format(ms: lengthMs)
|
||||
isPlaying = mediaPlayer.isPlaying
|
||||
|
|
@ -535,6 +534,137 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
if h > 0 { return String(format: "%d:%02d:%02d", h, m, s) }
|
||||
return String(format: "%d:%02d", m, s)
|
||||
}
|
||||
|
||||
// MARK: - PiP / audio
|
||||
|
||||
fileprivate func onPictureInPictureReady(_ controller: VLCPictureInPictureWindowControlling) {
|
||||
pipWindow = controller
|
||||
pipPossible = true
|
||||
controller.stateChangeEventHandler = { [weak self] isStarted in
|
||||
DispatchQueue.main.async {
|
||||
self?.pipActive = isStarted
|
||||
}
|
||||
}
|
||||
// Automatisch PiP bij app → achtergrond (Netflix-gedrag).
|
||||
if let obj = controller as? NSObject {
|
||||
for key in ["avPipController", "avPictureInPictureController", "_avPipController"] {
|
||||
if let av = obj.value(forKey: key) as? AVPictureInPictureController {
|
||||
if #available(iOS 14.2, *) {
|
||||
av.canStartPictureInPictureAutomaticallyFromInline = true
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func invalidatePipPlaybackState() {
|
||||
pipWindow?.invalidatePlaybackState()
|
||||
}
|
||||
|
||||
private func configureAudioSession() {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
do {
|
||||
try session.setCategory(.playback, mode: .moviePlayback, options: [])
|
||||
try session.setActive(true)
|
||||
} catch {
|
||||
// Speelt vaak nog steeds; PiP vereist wel background audio mode in Info.plist.
|
||||
}
|
||||
}
|
||||
|
||||
private func observeSceneForAutoPip() {
|
||||
sceneObserver = NotificationCenter.default.addObserver(
|
||||
forName: UIApplication.didEnterBackgroundNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] _ in
|
||||
guard let self, self.isPlaying, !self.pipActive, self.pipPossible else { return }
|
||||
self.startPictureInPicture()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension PlayerTrack {
|
||||
static let offId = "__off__"
|
||||
}
|
||||
|
||||
// MARK: - VLC drawable + Apple PiP bridge
|
||||
|
||||
final class VLCPipDrawableHost: NSObject, VLCDrawable, VLCPictureInPictureDrawable, VLCPictureInPictureMediaControlling {
|
||||
weak var controller: VLCPlayerController?
|
||||
private weak var container: UIView?
|
||||
private var videoSubview: UIView?
|
||||
|
||||
func attach(to view: UIView) {
|
||||
container = view
|
||||
view.backgroundColor = .black
|
||||
if let videoSubview, videoSubview.superview !== view {
|
||||
videoSubview.removeFromSuperview()
|
||||
view.addSubview(videoSubview)
|
||||
videoSubview.frame = view.bounds
|
||||
videoSubview.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
}
|
||||
|
||||
// VLCDrawable
|
||||
func addSubview(_ view: UIView) {
|
||||
videoSubview = view
|
||||
guard let container else { return }
|
||||
container.subviews.filter { $0 !== view }.forEach { $0.removeFromSuperview() }
|
||||
if view.superview !== container {
|
||||
container.addSubview(view)
|
||||
}
|
||||
view.frame = container.bounds
|
||||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
|
||||
var bounds: CGRect {
|
||||
container?.bounds ?? .zero
|
||||
}
|
||||
|
||||
// VLCPictureInPictureDrawable
|
||||
var mediaController: VLCPictureInPictureMediaControlling { self }
|
||||
|
||||
func pictureInPictureReady() -> (VLCPictureInPictureWindowControlling) -> Void {
|
||||
{ [weak self] pip in
|
||||
self?.controller?.onPictureInPictureReady(pip)
|
||||
}
|
||||
}
|
||||
|
||||
// VLCPictureInPictureMediaControlling
|
||||
func play() {
|
||||
controller?.mediaPlayer.play()
|
||||
}
|
||||
|
||||
func pause() {
|
||||
controller?.mediaPlayer.pause()
|
||||
}
|
||||
|
||||
@objc(seekBy:completion:)
|
||||
func seekBy(_ offset: Int64, completion: @escaping () -> Void) {
|
||||
guard let player = controller?.mediaPlayer else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
let ok = player.jump(withOffset: Int(offset), completion: completion)
|
||||
if !ok { completion() }
|
||||
}
|
||||
|
||||
func mediaLength() -> Int64 {
|
||||
Int64(controller?.mediaPlayer.media?.length.intValue ?? 0)
|
||||
}
|
||||
|
||||
func mediaTime() -> Int64 {
|
||||
Int64(controller?.mediaPlayer.time.intValue ?? 0)
|
||||
}
|
||||
|
||||
func isMediaSeekable() -> Bool {
|
||||
controller?.mediaPlayer.isSeekable ?? false
|
||||
}
|
||||
|
||||
func isMediaPlaying() -> Bool {
|
||||
controller?.mediaPlayer.isPlaying ?? false
|
||||
}
|
||||
}
|
||||
|
||||
struct VLCVideoView: UIViewRepresentable {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
import AVFoundation
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct VMCApp: App {
|
||||
@StateObject private var appModel = AppModel()
|
||||
|
||||
init() {
|
||||
let session = AVAudioSession.sharedInstance()
|
||||
try? session.setCategory(.playback, mode: .moviePlayback)
|
||||
try? session.setActive(true)
|
||||
}
|
||||
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
RootView()
|
||||
|
|
|
|||
|
|
@ -1,49 +1,21 @@
|
|||
#!/bin/zsh
|
||||
set -euo pipefail
|
||||
cd /tmp
|
||||
echo "Listing MobileVLCKit packages..."
|
||||
curl -fsSL https://download.videolan.org/pub/cocoapods/prod/ -o /tmp/vlc-prod.html
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
html = Path('/tmp/vlc-prod.html').read_text(errors='ignore')
|
||||
names = sorted(set(re.findall(r'href="(MobileVLCKit-3\.[^"]+)"', html)))
|
||||
for n in names[-25:]:
|
||||
print(n)
|
||||
PY
|
||||
# VLCKit 4.x (unified) — required for Apple Picture in Picture.
|
||||
# MobileVLCKit 3.7.x has NO PiP API.
|
||||
|
||||
# Prefer newest 3.5/3.6 if present else latest 3.x
|
||||
python3 - <<'PY'
|
||||
from pathlib import Path
|
||||
import re
|
||||
html = Path('/tmp/vlc-prod.html').read_text(errors='ignore')
|
||||
names = sorted(set(re.findall(r'href="(MobileVLCKit-3\.[^"]+)"', html)))
|
||||
pick = None
|
||||
for n in reversed(names):
|
||||
if n.startswith('MobileVLCKit-3.5') or n.startswith('MobileVLCKit-3.6'):
|
||||
pick = n
|
||||
break
|
||||
if not pick and names:
|
||||
pick = names[-1]
|
||||
print('PICK=' + (pick or ''))
|
||||
Path('/tmp/vlc-pick.txt').write_text(pick or '')
|
||||
PY
|
||||
URL='https://download.videolan.org/pub/cocoapods/unstable/VLCKit-4.0.0a19-d7597c1706-85a537d69.tar.xz'
|
||||
OUT=/tmp/VLCKit-4.0.0a19.tar.xz
|
||||
UNPACK=/tmp/vlckit4unpack
|
||||
|
||||
PICK=$(cat /tmp/vlc-pick.txt)
|
||||
if [ -z "$PICK" ]; then
|
||||
echo "No package found"
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloading $PICK ..."
|
||||
curl -fL --progress-bar "https://download.videolan.org/pub/cocoapods/prod/$PICK" -o "/tmp/$PICK"
|
||||
mkdir -p /tmp/vlcunpack
|
||||
rm -rf /tmp/vlcunpack/*
|
||||
case "$PICK" in
|
||||
*.zip) unzip -q "/tmp/$PICK" -d /tmp/vlcunpack ;;
|
||||
*.tar.xz|*.txz) tar -xJf "/tmp/$PICK" -C /tmp/vlcunpack ;;
|
||||
*.tar.bz2) tar -xjf "/tmp/$PICK" -C /tmp/vlcunpack ;;
|
||||
*) tar -xf "/tmp/$PICK" -C /tmp/vlcunpack ;;
|
||||
esac
|
||||
echo "Unpacked contents:"
|
||||
find /tmp/vlcunpack -maxdepth 3 -type d | head -40
|
||||
find /tmp/vlcunpack -name '*.framework' -o -name '*.xcframework' | head -20
|
||||
echo "Downloading VLCKit 4.0.0a19 (~500MB)…"
|
||||
curl -fL --progress-bar -o "$OUT" "$URL"
|
||||
|
||||
rm -rf "$UNPACK"
|
||||
mkdir -p "$UNPACK"
|
||||
echo "Unpacking…"
|
||||
tar -xJf "$OUT" -C "$UNPACK"
|
||||
|
||||
echo "Unpacked:"
|
||||
find "$UNPACK" -name 'VLCKit.xcframework' -type d | head -5
|
||||
test -d "$UNPACK/VLCKit-binary/VLCKit.xcframework"
|
||||
echo "OK — run: bash scripts/link-vlckit.sh"
|
||||
|
|
|
|||
|
|
@ -1,94 +1,29 @@
|
|||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
IOS="$HOME/Code/mediacluster/apps/ios"
|
||||
IOS="${HOME}/Code/mediacluster/apps/ios"
|
||||
mkdir -p "$IOS/Vendor"
|
||||
rm -rf "$IOS/Vendor/MobileVLCKit.xcframework"
|
||||
cp -R /tmp/vlcunpack/MobileVLCKit-binary/MobileVLCKit.xcframework "$IOS/Vendor/"
|
||||
ls "$IOS/Vendor/MobileVLCKit.xcframework"
|
||||
|
||||
python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import uuid, re
|
||||
SRC="/tmp/vlckit4unpack/VLCKit-binary/VLCKit.xcframework"
|
||||
if [ ! -d "$SRC" ]; then
|
||||
echo "Missing $SRC — run scripts/fetch-vlckit.sh first"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
pbx_path = Path.home() / "Code/mediacluster/apps/ios/VMC.xcodeproj/project.pbxproj"
|
||||
text = pbx_path.read_text()
|
||||
if "MobileVLCKit.xcframework" in text:
|
||||
print("Already linked")
|
||||
raise SystemExit(0)
|
||||
# Backup oude MobileVLCKit indien aanwezig
|
||||
if [ -d "$IOS/Vendor/MobileVLCKit.xcframework" ]; then
|
||||
mv "$IOS/Vendor/MobileVLCKit.xcframework" \
|
||||
"$IOS/Vendor/MobileVLCKit.xcframework.bak.$(date +%Y%m%d%H%M)" || true
|
||||
fi
|
||||
|
||||
def nid():
|
||||
return uuid.uuid4().hex[:24].upper()
|
||||
rm -rf "$IOS/Vendor/VLCKit.xcframework"
|
||||
cp -R "$SRC" "$IOS/Vendor/VLCKit.xcframework"
|
||||
|
||||
fw_ref, fw_build, embed_build, embed_phase = nid(), nid(), nid(), nid()
|
||||
HDR="$IOS/Vendor/VLCKit.xcframework/ios-arm64/VLCKit.framework/Headers/VLCDrawable.h"
|
||||
if ! grep -q 'VLCPictureInPictureDrawable' "$HDR"; then
|
||||
echo "ERROR: PiP headers missing in $HDR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
text = text.replace(
|
||||
"/* End PBXFileReference section */",
|
||||
f'\t\t{fw_ref} /* MobileVLCKit.xcframework */ = {{isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = MobileVLCKit.xcframework; path = Vendor/MobileVLCKit.xcframework; sourceTree = "<group>"; }};\n/* End PBXFileReference section */',
|
||||
)
|
||||
|
||||
text = text.replace(
|
||||
"/* End PBXBuildFile section */",
|
||||
(
|
||||
f"\t\t{fw_build} /* MobileVLCKit.xcframework in Frameworks */ = {{isa = PBXBuildFile; fileRef = {fw_ref} /* MobileVLCKit.xcframework */; }};\n"
|
||||
f"\t\t{embed_build} /* MobileVLCKit.xcframework in Embed Frameworks */ = {{isa = PBXBuildFile; fileRef = {fw_ref} /* MobileVLCKit.xcframework */; settings = {{ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }}; }};\n"
|
||||
"/* End PBXBuildFile section */"
|
||||
),
|
||||
)
|
||||
|
||||
text = re.sub(
|
||||
r"(isa = PBXFrameworksBuildPhase;\n\t\t\tbuildActionMask = 2147483647;\n\t\t\tfiles = \()\n(\t\t\t\);)",
|
||||
rf"\1\n\t\t\t\t{fw_build} /* MobileVLCKit.xcframework in Frameworks */,\n\2",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
|
||||
if "PBXCopyFilesBuildPhase" not in text:
|
||||
block = f"""/* Begin PBXCopyFilesBuildPhase section */
|
||||
\t\t{embed_phase} /* Embed Frameworks */ = {{
|
||||
\t\t\tisa = PBXCopyFilesBuildPhase;
|
||||
\t\t\tbuildActionMask = 2147483647;
|
||||
\t\t\tdstPath = "";
|
||||
\t\t\tdstSubfolderSpec = 10;
|
||||
\t\t\tfiles = (
|
||||
\t\t\t\t{embed_build} /* MobileVLCKit.xcframework in Embed Frameworks */,
|
||||
\t\t\t);
|
||||
\t\t\tname = "Embed Frameworks";
|
||||
\t\t\trunOnlyForDeploymentPostprocessing = 0;
|
||||
\t\t}};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
|
||||
"""
|
||||
text = text.replace("/* Begin PBXFrameworksBuildPhase section */", block + "/* Begin PBXFrameworksBuildPhase section */")
|
||||
|
||||
text = re.sub(
|
||||
r"(buildPhases = \(\n\t\t\t\t[A-F0-9]+ /\* Sources \*/,\n\t\t\t\t[A-F0-9]+ /\* Frameworks \*/,)",
|
||||
rf"\1\n\t\t\t\t{embed_phase} /* Embed Frameworks */,",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
|
||||
# Insert into root group children (before Products)
|
||||
text = re.sub(
|
||||
r"(children = \(\n\t\t\t\t[A-F0-9]+ /\* VMC \*/,)\n(\t\t\t\t[A-F0-9]+ /\* Products \*/,)",
|
||||
rf"\1\n\t\t\t\t{fw_ref} /* MobileVLCKit.xcframework */,\n\2",
|
||||
text,
|
||||
count=1,
|
||||
)
|
||||
|
||||
# Target build settings
|
||||
text = text.replace(
|
||||
"MARKETING_VERSION = 0.2.0;\n",
|
||||
(
|
||||
"MARKETING_VERSION = 0.2.0;\n"
|
||||
"\t\t\t\tFRAMEWORK_SEARCH_PATHS = (\n"
|
||||
'\t\t\t\t\t"$(inherited)",\n'
|
||||
'\t\t\t\t\t"$(PROJECT_DIR)/Vendor",\n'
|
||||
"\t\t\t\t);\n"
|
||||
"\t\t\t\tENABLE_BITCODE = NO;\n"
|
||||
'\t\t\t\tOTHER_LDFLAGS = ("$(inherited)", "-ObjC");\n'
|
||||
),
|
||||
)
|
||||
|
||||
pbx_path.write_text(text)
|
||||
print("Patched OK")
|
||||
PY
|
||||
echo "Linked Vendor/VLCKit.xcframework"
|
||||
du -sh "$IOS/Vendor/VLCKit.xcframework"
|
||||
grep -n 'VLCPictureInPictureDrawable' "$HDR" | head -3
|
||||
|
|
|
|||
Loading…
Reference in a new issue