Initial chromecast support.
|
@ -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>";
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 981 B After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 856 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 888 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 884 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 824 B After Width: | Height: | Size: 1.3 KiB |
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)))
|
||||||
|
|
|
@ -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"/>
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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")
|
||||||
|
|