Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add macOS/iOS external player referer support #397

Merged
merged 1 commit into from
Nov 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 20 additions & 5 deletions ios/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ import AVKit
binaryMessenger: controller.binaryMessenger)
channel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "openWithMime" {
if call.method == "openWithReferer" {
guard let args = call.arguments else { return }
if let myArgs = args as? [String: Any],
let url = myArgs["url"] as? String,
let mimeType = myArgs["mimeType"] as? String {
self.openVideoWithMime(url: url, mimeType: mimeType)
let referer = myArgs["referer"] as? String {
self.openVideoWithReferer(url: url, referer: referer)
}
result(nil)
} else {
Expand All @@ -29,9 +29,17 @@ import AVKit
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

private func openVideoWithMime(url: String, mimeType: String) {
// TODO: ADD VLC SUPPORT
// VLC can be downloaded from iOS App Store, but don't know how to build selectable app lists, while checking if it is installled.
// VLC supports more video formats than AVPlayer but does not support referer while AVPlayer does
private func openVideoWithReferer(url: String, referer: String) {
if let videoUrl = URL(string: url) {
let player = AVPlayer(url: videoUrl)
let headers: [String: String] = [
"Referer": referer,
]
let asset = AVURLAsset(url: videoUrl, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
let playerItem = AVPlayerItem(asset: asset)
let player = AVPlayer(playerItem: playerItem)
let playerViewController = AVPlayerViewController()
playerViewController.player = player
playerViewController.videoGravity = AVLayerVideoGravity.resizeAspect
Expand All @@ -40,5 +48,12 @@ import AVKit
playerViewController.player!.play()
})
}

// guard let appURL = URL(string: "vlc-x-callback://x-callback-url/stream?url=" + url) else {
// return
// }
// if UIApplication.shared.canOpenURL(appURL) && referer.isEmpty {
// UIApplication.shared.open(appURL, options: [:], completionHandler: nil)
// }
}
}
22 changes: 13 additions & 9 deletions ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
Expand All @@ -22,8 +24,19 @@
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>vlc-x-callback</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
Expand All @@ -41,14 +54,5 @@
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
</dict>
</plist>
6 changes: 1 addition & 5 deletions lib/pages/player/player_item.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1243,12 +1243,8 @@ class _PlayerItemState extends State<PlayerItem>
color: Colors.white,
icon: const Icon(Icons.cast),
onPressed: () {
if (videoPageController.currentPlugin.referer == '') {
playerController.pause();
RemotePlay().castVideo(context);
} else {
SmartDialog.showToast('暂不支持该播放源', displayType: SmartToastType.onlyRefresh);
}
RemotePlay().castVideo(context, videoPageController.currentPlugin.referer);
},
),
// 追番
Expand Down
41 changes: 33 additions & 8 deletions lib/utils/remote.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,13 @@ import 'package:url_launcher/url_launcher_string.dart';
import '../pages/player/player_controller.dart';

class RemotePlay {
// 注意:仍需开发 iOS/macOS/Linux 设备的远程播放功能。
// 注意:仍需开发 iOS/Linux 设备的远程播放功能。
// 在 Windows 设备上,对于其他可能的实现,使用 scheme 的方案没有效果。VLC / PotPlayer 等主流播放器更倾向于使用 CLI 命令。
// 而对于 iOS / Mac 设备,由于没有设备,无法进行开发与验证。
// 可行的 iOS / Mac 处理代码,请参见 ios/Runner/AppDelegate.swift 的注释部分。
// 可行的 iOS 处理代码,请参见 ios/Runner/AppDelegate.swift 的注释部分。

static const platform = MethodChannel('com.predidit.kazumi/intent');

castVideo(BuildContext context) async {
castVideo(BuildContext context, String referer) async {
final searcher = DLNAManager();
final dlna = await searcher.start();
final String video = Modular.get<PlayerController>().videoUrl;
Expand All @@ -39,7 +38,7 @@ class RemotePlay {
actions: [
TextButton(
onPressed: () async {
if (Platform.isAndroid || Platform.isWindows || Platform.isMacOS || Platform.isIOS) {
if (Platform.isAndroid || Platform.isWindows && referer.isEmpty) {
if (await _launchURLWithMIME(video, 'video/mp4')) {
SmartDialog.dismiss();
SmartDialog.showToast('尝试唤起外部播放器',
Expand All @@ -48,7 +47,16 @@ class RemotePlay {
SmartDialog.showToast('唤起外部播放器失败',
displayType: SmartToastType.onlyRefresh);
}
} else if (Platform.isLinux) {
} else if (Platform.isMacOS || Platform.isIOS) {
if (await _launchURLWithReferer(video, referer)) {
SmartDialog.dismiss();
SmartDialog.showToast('尝试唤起外部播放器',
displayType: SmartToastType.onlyRefresh);
} else {
SmartDialog.showToast('唤起外部播放器失败',
displayType: SmartToastType.onlyRefresh);
}
} else if (Platform.isLinux && referer.isEmpty) {
SmartDialog.dismiss();
if (await canLaunchUrlString(video)) {
launchUrlString(video);
Expand All @@ -59,8 +67,13 @@ class RemotePlay {
displayType: SmartToastType.onlyRefresh);
}
} else {
SmartDialog.showToast('暂不支持该设备',
displayType: SmartToastType.onlyRefresh);
if (referer.isEmpty) {
SmartDialog.showToast('暂不支持该设备',
displayType: SmartToastType.onlyRefresh);
} else {
SmartDialog.showToast('暂不支持该规则',
displayType: SmartToastType.onlyRefresh);
}
}
},
child: const Text('外部播放'),
Expand Down Expand Up @@ -172,4 +185,16 @@ class RemotePlay {
return false;
}
}

Future<bool> _launchURLWithReferer(String url, String referer) async {
try {
await platform.invokeMethod(
'openWithReferer', <String, String>{'url': url, 'referer': referer});
return true;
} on PlatformException catch (e) {
KazumiLogger()
.log(Level.error, "Failed to open with referer: '${e.message}'.");
return false;
}
}
}
55 changes: 42 additions & 13 deletions macos/Runner/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,18 +12,19 @@ class AppDelegate: FlutterAppDelegate {
var playerView: AVPlayerView!
var player: AVPlayer?
var videoUrl: URL?
var httpReferer: String = ""

override func applicationDidFinishLaunching(_ notification: Notification) {
let controller : FlutterViewController = mainFlutterWindow?.contentViewController as! FlutterViewController
let channel = FlutterMethodChannel.init(name: "com.predidit.kazumi/intent", binaryMessenger: controller.engine.binaryMessenger)
channel.setMethodCallHandler({
(_ call: FlutterMethodCall, _ result: FlutterResult) -> Void in
if call.method == "openWithMime" {
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
if call.method == "openWithReferer" {
guard let args = call.arguments else { return }
if let myArgs = args as? [String: Any],
let url = myArgs["url"] as? String,
let mimeType = myArgs["mimeType"] as? String {
self.openVideoWithMime(url: url, mimeType: mimeType)
let referer = myArgs["referer"] as? String {
self.openVideoWithReferer(url: url, referer: referer)
}
result(nil)
} else {
Expand All @@ -32,10 +33,8 @@ class AppDelegate: FlutterAppDelegate {
});
}

func findApplicationsByMimeType(mimeType: String) -> [URL] {

let fileExtension = mimeType.components(separatedBy: "/").last ?? ""
let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.\(fileExtension)")
func findApplicationsByMimeType() -> [URL] {
let tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("temp.mp4")

FileManager.default.createFile(atPath: tempFileURL.path, contents: nil, attributes: nil)

Expand All @@ -61,17 +60,17 @@ class AppDelegate: FlutterAppDelegate {
}
}

private func openVideoWithMime(url: String, mimeType: String) {
private func openVideoWithReferer(url: String, referer: String) {
videoUrl = URL(string: url)
httpReferer = referer

let selectMenu = NSMenu()
let appLists = findApplicationsByMimeType(mimeType: mimeType)
let appLists = findApplicationsByMimeType()

/* AVPlayer menu item start */
let menuItem = NSMenuItem()
menuItem.attributedTitle = NSAttributedString(string: "AVPlayer", attributes: [.font: NSFont.systemFont(ofSize: 14)])
menuItem.action = #selector(openWithAVPlayer)
menuItem.toolTip = "macOS自带播放器,部分视频源有兼容问题"

let icon = NSWorkspace.shared.icon(forFile: "/System/Applications/Preview.app")
icon.size = NSSize(width: 16, height: 16)
Expand All @@ -90,7 +89,11 @@ class AppDelegate: FlutterAppDelegate {

let menuItem = NSMenuItem()
menuItem.attributedTitle = NSAttributedString(string: "\(appName).app", attributes: [.font: NSFont.systemFont(ofSize: 14)])
menuItem.action = #selector(openWithSelectedApp(_:))
if appName == "VLC" {
menuItem.action = #selector(openWithVLC(_:))
} else {
menuItem.action = #selector(openWithSelectedApp(_:))
}
menuItem.representedObject = "/Applications/\(appName).app/Contents/MacOS/\(appName)"

let icon = NSWorkspace.shared.icon(forFile: "/Applications/\(appName).app")
Expand All @@ -116,12 +119,24 @@ class AppDelegate: FlutterAppDelegate {
window.contentView?.addSubview(playerView)
window.delegate = self

player = AVPlayer(url: videoUrl!)
let headers: [String: String] = [
"Referer": httpReferer,
]
let asset = AVURLAsset(url: videoUrl!, options: ["AVURLAssetHTTPHeaderFieldsKey": headers])
let playerItem = AVPlayerItem(asset: asset)
player = AVPlayer(playerItem: playerItem)
playerView.player = player
playerView.player?.play()
}

@objc func openWithSelectedApp (_ sender: NSMenuItem) {
if !httpReferer.isEmpty {
let alert = NSAlert()
alert.messageText = "打开应用失败"
alert.informativeText = "该应用不支持 Referer 请求头,打开失败。请使用 AVPlayer/VLC 打开或更换规则。"
alert.runModal()
return
}
if let selectedApp = sender.representedObject {
let process = Process()
process.launchPath = selectedApp as? String
Expand All @@ -134,6 +149,20 @@ class AppDelegate: FlutterAppDelegate {
}
}
}

@objc func openWithVLC (_ sender: NSMenuItem) {
if let selectedApp = sender.representedObject {
let process = Process()
process.launchPath = selectedApp as? String
process.arguments = [videoUrl!.absoluteString, ":http-referrer=" + httpReferer]

do {
try process.run()
} catch {
print("Failed to open app: \(error)")
}
}
}
}

extension AppDelegate: NSWindowDelegate {
Expand Down