Initial chromecast support.

This commit is contained in:
Aiden Vigue 2021-06-19 13:50:35 -04:00
parent 57e35950ee
commit b5571639aa
No known key found for this signature in database
GPG Key ID: B9A09843AB079D5B
23 changed files with 285 additions and 65 deletions

View File

@ -646,12 +646,12 @@
539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */, 539B2DA4263BA5B8007FF1A4 /* SettingsView.swift */,
535BAEA4264A151C005FA86D /* VideoPlayer.swift */, 535BAEA4264A151C005FA86D /* VideoPlayer.swift */,
53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */, 53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */,
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */, 53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
53DE4BD1267098F300739748 /* SearchBarView.swift */, 53DE4BD1267098F300739748 /* SearchBarView.swift */,
625CB5672678B6FB00530A6E /* SplashView.swift */, 625CB5672678B6FB00530A6E /* SplashView.swift */,
625CB56B2678C0FD00530A6E /* MainTabView.swift */, 625CB56B2678C0FD00530A6E /* MainTabView.swift */,
625CB56E2678C23300530A6E /* HomeView.swift */, 625CB56E2678C23300530A6E /* HomeView.swift */,
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
); );
path = JellyfinPlayer; path = JellyfinPlayer;
sourceTree = "<group>"; sourceTree = "<group>";

View File

@ -29,7 +29,7 @@
} }
}, },
{ {
"package": "JellyfinAPI", "package": "jellyfin-sdk-swift",
"repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift", "repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift",
"state": { "state": {
"branch": "main", "branch": "main",
@ -38,7 +38,7 @@
} }
}, },
{ {
"package": "KeychainSwift", "package": "keychain-swift",
"repositoryURL": "https://github.com/evgenyneu/keychain-swift", "repositoryURL": "https://github.com/evgenyneu/keychain-swift",
"state": { "state": {
"branch": null, "branch": null,
@ -83,7 +83,7 @@
} }
}, },
{ {
"package": "SwiftProtobuf", "package": "swift-protobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git", "repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": { "state": {
"branch": null, "branch": null,
@ -92,7 +92,7 @@
} }
}, },
{ {
"package": "Introspect", "package": "SwiftUI-Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect", "repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
"state": { "state": {
"branch": null, "branch": null,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 981 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 856 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 888 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 884 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 824 B

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -26,6 +26,12 @@
<true/> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoadsForMedia</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true/> <true/>
</dict> </dict>

View File

@ -34,7 +34,7 @@ struct ItemView: View {
.statusBar(hidden: true) .statusBar(hidden: true)
.edgesIgnoringSafeArea(.all) .edgesIgnoringSafeArea(.all)
.prefersHomeIndicatorAutoHidden(true) .prefersHomeIndicatorAutoHidden(true)
}, isActive: $videoPlayerItem.shouldShowPlayer) { }.supportedOrientations(.landscape), isActive: $videoPlayerItem.shouldShowPlayer) {
EmptyView() EmptyView()
} }
VStack { VStack {

View File

@ -156,7 +156,7 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
kCFStreamPropertyShouldCloseNativeSocket as String: true kCFStreamPropertyShouldCloseNativeSocket as String: true
] ]
CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream) CFStreamCreatePairWithSocketToHost(nil, self.device.hostName as CFString, UInt32(self.device.port), &readStream, &writeStream)
guard let readStreamRetained = readStream?.takeRetainedValue() else { guard let readStreamRetained = readStream?.takeRetainedValue() else {
throw CastError.connection("Unable to create input stream") throw CastError.connection("Unable to create input stream")
@ -255,7 +255,6 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
let message = try CastMessage(serializedData: payload) let message = try CastMessage(serializedData: payload)
guard let channel = channels[message.namespace] else { guard let channel = channels[message.namespace] else {
print("No channel attached for namespace \(message.namespace)")
return return
} }
@ -346,7 +345,6 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
namespace: request.namespace, namespace: request.namespace,
sourceId: senderName, sourceId: senderName,
destinationId: request.destinationId) destinationId: request.destinationId)
try write(data: messageData) try write(data: messageData)
} catch { } catch {
callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription))) callResponseHandler(for: request.id, with: Result(error: .request(error.localizedDescription)))

View File

@ -17,6 +17,30 @@
<view key="view" autoresizesSubviews="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IQg-r0-AeH"> <view key="view" autoresizesSubviews="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="IQg-r0-AeH">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/> <rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<subviews> <subviews>
<view autoresizesSubviews="NO" tag="1" contentMode="scaleToFill" fixedFrame="YES" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Fa2-nx-Dzx" userLabel="VideoCastingContentView">
<rect key="frame" x="31" y="0.0" width="834" height="414"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<textView clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" text="Streaming to Chromecast" textAlignment="center" translatesAutoresizingMaskIntoConstraints="NO" id="CJ6-bw-cyX">
<rect key="frame" x="224" y="105" width="387" height="128"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<constraints>
<constraint firstAttribute="height" constant="128" id="W6f-zI-T4b"/>
</constraints>
<color key="textColor" white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<fontDescription key="fontDescription" type="system" pointSize="28"/>
<textInputTraits key="textInputTraits" autocapitalizationType="sentences"/>
</textView>
</subviews>
<viewLayoutGuide key="safeArea" id="by1-Hb-CaQ"/>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<gestureRecognizers/>
<constraints>
<constraint firstItem="CJ6-bw-cyX" firstAttribute="leading" secondItem="by1-Hb-CaQ" secondAttribute="leading" constant="211" id="7lb-qN-MqL"/>
<constraint firstItem="by1-Hb-CaQ" firstAttribute="trailing" secondItem="CJ6-bw-cyX" secondAttribute="trailing" constant="210" id="lKv-tL-HF2"/>
<constraint firstItem="CJ6-bw-cyX" firstAttribute="top" secondItem="by1-Hb-CaQ" secondAttribute="top" constant="105" id="xTG-rr-kLR"/>
</constraints>
</view>
<view autoresizesSubviews="NO" tag="1" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tsh-rC-BwO" userLabel="VideoContentView"> <view autoresizesSubviews="NO" tag="1" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Tsh-rC-BwO" userLabel="VideoContentView">
<rect key="frame" x="31" y="0.0" width="834" height="414"/> <rect key="frame" x="31" y="0.0" width="834" height="414"/>
<viewLayoutGuide key="safeArea" id="aVY-BC-PZU"/> <viewLayoutGuide key="safeArea" id="aVY-BC-PZU"/>
@ -214,7 +238,7 @@
</scene> </scene>
</scenes> </scenes>
<resources> <resources>
<image name="CastDisconnected" width="20" height="20"/> <image name="CastDisconnected" width="24" height="24"/>
<image name="chevron.backward" catalog="system" width="96" height="128"/> <image name="chevron.backward" catalog="system" width="96" height="128"/>
<image name="gear" catalog="system" width="128" height="119"/> <image name="gear" catalog="system" width="128" height="119"/>
<image name="gobackward.15" catalog="system" width="121" height="128"/> <image name="gobackward.15" catalog="system" width="121" height="128"/>

View File

@ -10,6 +10,7 @@ import MobileVLCKit
import JellyfinAPI import JellyfinAPI
import MediaPlayer import MediaPlayer
import Combine import Combine
import SwiftyJSON
struct Subtitle { struct Subtitle {
var name: String var name: String
@ -24,6 +25,11 @@ struct AudioTrack {
var id: Int32 var id: Int32
} }
enum PlayerDestination {
case remote
case local
}
class PlaybackItem: ObservableObject { class PlaybackItem: ObservableObject {
@Published var videoType: PlayMethod = .directPlay @Published var videoType: PlayMethod = .directPlay
@Published var videoUrl: URL = URL(string: "https://example.com")! @Published var videoUrl: URL = URL(string: "https://example.com")!
@ -35,7 +41,7 @@ protocol PlayerViewControllerDelegate: AnyObject {
func exitPlayer(_ viewController: PlayerViewController) func exitPlayer(_ viewController: PlayerViewController)
} }
class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate { class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate, CastClientDelegate {
weak var delegate: PlayerViewControllerDelegate? weak var delegate: PlayerViewControllerDelegate?
@ -62,7 +68,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
var lastTime: Float = 0.0 var lastTime: Float = 0.0
var startTime: Int = 0 var startTime: Int = 0
var controlsAppearTime: Double = 0 var controlsAppearTime: Double = 0
var discoveredCastDevices: [CastDevice] = []
var discoveredCastDevices: [CastDevice] = [] //not private due to VPCDS using it.
var selectedCastDevice: CastDevice? //same here
private var castClient: CastClient?
private var playerDestination: PlayerDestination = .local;
private var castAppTransportID: String = "";
private var remotePlayIsPlaying: Bool = false;
private var remotePlaySeekState: Int = 0;
private let castScanner: CastDeviceScanner = CastDeviceScanner();
var selectedAudioTrack: Int32 = -1 { var selectedAudioTrack: Int32 = -1 {
didSet { didSet {
@ -85,8 +99,10 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// MARK: IBActions // MARK: IBActions
@IBAction func seekSliderStart(_ sender: Any) { @IBAction func seekSliderStart(_ sender: Any) {
sendProgressReport(eventName: "pause") if(playerDestination == .local) {
mediaPlayer.pause() sendProgressReport(eventName: "pause")
mediaPlayer.pause()
}
} }
@IBAction func seekSliderValueChanged(_ sender: Any) { @IBAction func seekSliderValueChanged(_ sender: Any) {
@ -111,53 +127,87 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Scrub is value from 0..1 - find position in video and add / or remove. // Scrub is value from 0..1 - find position in video and add / or remove.
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration) let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let offset = secondsScrubbedTo - videoPosition let offset = secondsScrubbedTo - videoPosition
mediaPlayer.play()
if offset > 0 { if(playerDestination == .local) {
mediaPlayer.jumpForward(Int32(offset)/1000) mediaPlayer.play()
} else { if offset > 0 {
mediaPlayer.jumpBackward(Int32(abs(offset))/1000) mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
sendProgressReport(eventName: "unpause")
} }
sendProgressReport(eventName: "unpause")
} }
@IBAction func exitButtonPressed(_ sender: Any) { @IBAction func exitButtonPressed(_ sender: Any) {
sendStopReport() sendStopReport()
mediaPlayer.stop() mediaPlayer.stop()
if(playerDestination == .remote) {
castClient?.stopCurrentApp()
castClient?.disconnect()
castClient = nil
selectedCastDevice = nil
playerDestination = .local
}
delegate?.exitPlayer(self) delegate?.exitPlayer(self)
} }
@IBAction func controlViewTapped(_ sender: Any) { @IBAction func controlViewTapped(_ sender: Any) {
videoControlsView.isHidden = true if(playerDestination == .local) {
videoControlsView.isHidden = true
}
} }
@IBAction func contentViewTapped(_ sender: Any) { @IBAction func contentViewTapped(_ sender: Any) {
videoControlsView.isHidden = false if(playerDestination == .local) {
controlsAppearTime = CACurrentMediaTime() videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime()
}
} }
@IBAction func jumpBackTapped(_ sender: Any) { @IBAction func jumpBackTapped(_ sender: Any) {
if paused == false { if paused == false {
mediaPlayer.jumpBackward(15) if(playerDestination == .local) {
mediaPlayer.jumpBackward(15)
} else {
}
} }
} }
@IBAction func jumpForwardTapped(_ sender: Any) { @IBAction func jumpForwardTapped(_ sender: Any) {
if paused == false { if paused == false {
mediaPlayer.jumpForward(30) if(playerDestination == .local) {
mediaPlayer.jumpForward(30)
} else {
}
} }
} }
@IBOutlet weak var mainActionButton: UIButton! @IBOutlet weak var mainActionButton: UIButton!
@IBAction func mainActionButtonPressed(_ sender: Any) { @IBAction func mainActionButtonPressed(_ sender: Any) {
print(mediaPlayer.state.rawValue)
if paused { if paused {
mediaPlayer.play() if(playerDestination == .local) {
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) mediaPlayer.play()
paused = false mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false
} else {
sendCastCommand(cmd: "Unpause")
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false
}
} else { } else {
mediaPlayer.pause() if(playerDestination == .local) {
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) mediaPlayer.pause()
paused = true mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
paused = true
} else {
sendCastCommand(cmd: "Pause")
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
paused = true
}
} }
} }
@ -175,19 +225,32 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
} }
} }
//MARK: Cast start
@IBAction func castButtonPressed(_ sender: Any) { @IBAction func castButtonPressed(_ sender: Any) {
castDeviceVC = VideoPlayerCastDeviceSelectorView() if(selectedCastDevice == nil) {
castDeviceVC?.delegate = self castDeviceVC = VideoPlayerCastDeviceSelectorView()
castDeviceVC?.delegate = self
castDeviceVC?.modalPresentationStyle = .popover castDeviceVC?.modalPresentationStyle = .popover
castDeviceVC?.popoverPresentationController?.sourceView = castButton castDeviceVC?.popoverPresentationController?.sourceView = castButton
// Present the view controller (in a popover). // Present the view controller (in a popover).
self.present(castDeviceVC!, animated: true) { self.present(castDeviceVC!, animated: true) {
print("popover visible, pause playback") print("popover visible, pause playback")
self.mediaPlayer.pause() self.mediaPlayer.pause()
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
}
} else {
castClient?.stopCurrentApp()
castClient?.disconnect()
selectedCastDevice = nil;
castClient = nil;
self.castButton.isEnabled = true
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
//disconnect cast device.
} }
} }
@ -196,11 +259,105 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
self.mediaPlayer.play() self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
} }
func castDeviceChanged() {
if(selectedCastDevice != nil) {
castClient = CastClient(device: selectedCastDevice!)
castClient!.delegate = self
castClient!.connect()
}
}
func sendCastCommand(cmd: String) {
let payload: [String: Any] = [
"options": [],
"command": cmd,
"userId": SessionManager.current.user.user_id!,
"deviceId": SessionManager.current.deviceID,
"accessToken": SessionManager.current.accessToken,
"serverAddress": ServerEnvironment.current.server.baseURI!,
"serverId": ServerEnvironment.current.server.server_id!,
"serverVersion": "10.8.0",
"receiverName": self.selectedCastDevice!.name
]
let req = CastRequest(id: castClient!.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: castAppTransportID, payload: payload)
castClient!.send(req, response: nil)
}
func castClient(_ client: CastClient, connectionTo device: CastDevice, didFailWith error: Error?) {
dump(error)
}
func castClient(_ client: CastClient, willConnectTo device: CastDevice) {
print("Connecting")
mediaPlayer.pause()
castScanner.stopScanning()
self.castButton.setImage(UIImage(named: "CastConnecting1"), for: .normal)
}
func castClient(_ client: CastClient, didConnectTo device: CastDevice) {
print("Connected")
self.castButton.setImage(UIImage(named: "CastConnected"), for: .normal)
//Launch player
client.launch(appId: "F007D354") { result in
switch result {
case .success(let app):
// here you would probably call client.load() to load some media
let payload: [String: Any] = [
"options": [
"items": [[
"Id": self.manifest.id!,
"ServerId": ServerEnvironment.current.server.server_id!,
"Name": self.manifest.name!,
"Type": self.manifest.type!,
"MediaType": self.manifest.mediaType!,
"IsFolder": self.manifest.isFolder!
]]
],
"command": "PlayNow",
"userId": SessionManager.current.user.user_id!,
"deviceId": SessionManager.current.deviceID,
"accessToken": SessionManager.current.accessToken,
"serverAddress": ServerEnvironment.current.server.baseURI!,
"serverId": ServerEnvironment.current.server.server_id!,
"serverVersion": "10.8.0",
"receiverName": self.selectedCastDevice!.name,
"subtitleBurnIn": false
]
self.castAppTransportID = app.transportId
let req = CastRequest(id: client.nextRequestId(), namespace: "urn:x-cast:com.connectsdk", destinationId: app.transportId, payload: payload)
client.send(req, response: self.castResponseHandler)
case .failure(let error):
print(error)
}
}
//Hide VLC player
videoContentView.isHidden = true;
playerDestination = .remote;
}
func castClient(_ client: CastClient, didDisconnectFrom device: CastDevice) {
print("Disconnected")
castScanner.startScanning()
playerDestination = .local;
videoContentView.isHidden = false;
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
}
func castResponseHandler(result: Result<JSON, CastError>) {
dump(result)
}
//MARK: Cast End
func settingsPopoverDismissed() { func settingsPopoverDismissed() {
optionsVC?.dismiss(animated: true, completion: nil) optionsVC?.dismiss(animated: true, completion: nil)
self.mediaPlayer.play() if(playerDestination == .local) {
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
}
} }
override var supportedInterfaceOrientations: UIInterfaceOrientationMask { override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
@ -221,31 +378,45 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Add handler for Pause Command // Add handler for Pause Command
commandCenter.pauseCommand.addTarget { _ in commandCenter.pauseCommand.addTarget { _ in
self.mediaPlayer.pause() if(self.playerDestination == .local) {
self.sendProgressReport(eventName: "pause") self.mediaPlayer.pause()
self.sendProgressReport(eventName: "pause")
} else {
self.sendCastCommand(cmd: "Pause")
}
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
return .success return .success
} }
// Add handler for Play command // Add handler for Play command
commandCenter.playCommand.addTarget { _ in commandCenter.playCommand.addTarget { _ in
self.mediaPlayer.play() if(self.playerDestination == .local) {
self.sendProgressReport(eventName: "unpause") self.mediaPlayer.play()
self.sendProgressReport(eventName: "unpause")
} else {
self.sendCastCommand(cmd: "Unpause")
}
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal) self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
return .success return .success
} }
// Add handler for FF command // Add handler for FF command
commandCenter.seekForwardCommand.addTarget { _ in commandCenter.seekForwardCommand.addTarget { _ in
self.mediaPlayer.jumpForward(30) if(self.playerDestination == .local) {
self.sendProgressReport(eventName: "timeupdate") self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
} else {
}
return .success return .success
} }
// Add handler for RW command // Add handler for RW command
commandCenter.seekBackwardCommand.addTarget { _ in commandCenter.seekBackwardCommand.addTarget { _ in
self.mediaPlayer.jumpBackward(15) if(self.playerDestination == .local) {
self.sendProgressReport(eventName: "timeupdate") self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
}
return .success return .success
} }
@ -255,15 +426,20 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent { if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
let targetSeconds = event.positionTime let targetSeconds = event.positionTime
let videoPosition = Double(self.mediaPlayer.time.intValue) let videoPosition = Double(self.mediaPlayer.time.intValue)
let offset = targetSeconds - videoPosition let offset = targetSeconds - videoPosition
if offset > 0 {
self.mediaPlayer.jumpForward(Int32(offset)/1000) if(self.playerDestination == .local) {
if offset > 0 {
self.mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
self.sendProgressReport(eventName: "unpause")
} else { } else {
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
} }
self.sendProgressReport(eventName: "unpause")
return .success return .success
} else { } else {
@ -299,11 +475,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
} }
func mediaHasStartedPlaying() { func mediaHasStartedPlaying() {
let scanner = CastDeviceScanner() NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: castScanner, queue: nil) { _ in
self.discoveredCastDevices = self.castScanner.devices
NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: scanner, queue: nil) { _ in if !self.castScanner.devices.isEmpty {
self.discoveredCastDevices = scanner.devices
if !scanner.devices.isEmpty {
self.castButton.isEnabled = true self.castButton.isEnabled = true
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal) self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
} else { } else {
@ -312,7 +486,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
} }
} }
scanner.startScanning() castScanner.startScanning()
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {

View File

@ -43,7 +43,25 @@ struct VideoPlayerCastDeviceSelector: View {
var body: some View { var body: some View {
NavigationView { NavigationView {
List(delegate.discoveredCastDevices, id: \.id) { device in List(delegate.discoveredCastDevices, id: \.id) { device in
Text(device.name) HStack() {
Text("\(device.name)")
.font(.subheadline)
.fontWeight(.medium)
Spacer()
Button {
delegate.selectedCastDevice = device
self.delegate?.castDeviceChanged()
self.delegate?.castPopoverDismissed()
} label: {
HStack() {
Text("Connect")
.font(.caption)
.fontWeight(.medium)
Image(systemName: "bonjour")
.font(.caption)
}
}
}
} }
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.navigationTitle("Select Cast Destination") .navigationTitle("Select Cast Destination")