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 */,
535BAEA4264A151C005FA86D /* VideoPlayer.swift */,
53313B8F265EEA6D00947AA3 /* VideoPlayer.storyboard */,
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
53F8377C265FF67C00F456B3 /* VideoPlayerSettingsView.swift */,
53DE4BD1267098F300739748 /* SearchBarView.swift */,
625CB5672678B6FB00530A6E /* SplashView.swift */,
625CB56B2678C0FD00530A6E /* MainTabView.swift */,
625CB56E2678C23300530A6E /* HomeView.swift */,
532E68CE267D9F6B007B9F13 /* VideoPlayerCastDeviceSelector.swift */,
);
path = JellyfinPlayer;
sourceTree = "<group>";

View File

@ -29,7 +29,7 @@
}
},
{
"package": "JellyfinAPI",
"package": "jellyfin-sdk-swift",
"repositoryURL": "https://github.com/jellyfin/jellyfin-sdk-swift",
"state": {
"branch": "main",
@ -38,7 +38,7 @@
}
},
{
"package": "KeychainSwift",
"package": "keychain-swift",
"repositoryURL": "https://github.com/evgenyneu/keychain-swift",
"state": {
"branch": null,
@ -83,7 +83,7 @@
}
},
{
"package": "SwiftProtobuf",
"package": "swift-protobuf",
"repositoryURL": "https://github.com/apple/swift-protobuf.git",
"state": {
"branch": null,
@ -92,7 +92,7 @@
}
},
{
"package": "Introspect",
"package": "SwiftUI-Introspect",
"repositoryURL": "https://github.com/siteline/SwiftUI-Introspect",
"state": {
"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/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoadsForMedia</key>
<true/>
<key>NSAllowsArbitraryLoadsInWebContent</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

View File

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

View File

@ -156,7 +156,7 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
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 {
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)
guard let channel = channels[message.namespace] else {
print("No channel attached for namespace \(message.namespace)")
return
}
@ -346,7 +345,6 @@ public final class CastClient: NSObject, RequestDispatchable, Channelable {
namespace: request.namespace,
sourceId: senderName,
destinationId: request.destinationId)
try write(data: messageData)
} catch {
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">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<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">
<rect key="frame" x="31" y="0.0" width="834" height="414"/>
<viewLayoutGuide key="safeArea" id="aVY-BC-PZU"/>
@ -214,7 +238,7 @@
</scene>
</scenes>
<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="gear" catalog="system" width="128" height="119"/>
<image name="gobackward.15" catalog="system" width="121" height="128"/>

View File

@ -10,6 +10,7 @@ import MobileVLCKit
import JellyfinAPI
import MediaPlayer
import Combine
import SwiftyJSON
struct Subtitle {
var name: String
@ -24,6 +25,11 @@ struct AudioTrack {
var id: Int32
}
enum PlayerDestination {
case remote
case local
}
class PlaybackItem: ObservableObject {
@Published var videoType: PlayMethod = .directPlay
@Published var videoUrl: URL = URL(string: "https://example.com")!
@ -35,7 +41,7 @@ protocol PlayerViewControllerDelegate: AnyObject {
func exitPlayer(_ viewController: PlayerViewController)
}
class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate {
class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDelegate, CastClientDelegate {
weak var delegate: PlayerViewControllerDelegate?
@ -62,7 +68,15 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
var lastTime: Float = 0.0
var startTime: Int = 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 {
didSet {
@ -85,8 +99,10 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// MARK: IBActions
@IBAction func seekSliderStart(_ sender: Any) {
sendProgressReport(eventName: "pause")
mediaPlayer.pause()
if(playerDestination == .local) {
sendProgressReport(eventName: "pause")
mediaPlayer.pause()
}
}
@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.
let secondsScrubbedTo = round(Double(seekSlider.value) * videoDuration)
let offset = secondsScrubbedTo - videoPosition
mediaPlayer.play()
if offset > 0 {
mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
if(playerDestination == .local) {
mediaPlayer.play()
if offset > 0 {
mediaPlayer.jumpForward(Int32(offset)/1000)
} else {
mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
sendProgressReport(eventName: "unpause")
}
sendProgressReport(eventName: "unpause")
}
@IBAction func exitButtonPressed(_ sender: Any) {
sendStopReport()
mediaPlayer.stop()
if(playerDestination == .remote) {
castClient?.stopCurrentApp()
castClient?.disconnect()
castClient = nil
selectedCastDevice = nil
playerDestination = .local
}
delegate?.exitPlayer(self)
}
@IBAction func controlViewTapped(_ sender: Any) {
videoControlsView.isHidden = true
if(playerDestination == .local) {
videoControlsView.isHidden = true
}
}
@IBAction func contentViewTapped(_ sender: Any) {
videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime()
if(playerDestination == .local) {
videoControlsView.isHidden = false
controlsAppearTime = CACurrentMediaTime()
}
}
@IBAction func jumpBackTapped(_ sender: Any) {
if paused == false {
mediaPlayer.jumpBackward(15)
if(playerDestination == .local) {
mediaPlayer.jumpBackward(15)
} else {
}
}
}
@IBAction func jumpForwardTapped(_ sender: Any) {
if paused == false {
mediaPlayer.jumpForward(30)
if(playerDestination == .local) {
mediaPlayer.jumpForward(30)
} else {
}
}
}
@IBOutlet weak var mainActionButton: UIButton!
@IBAction func mainActionButtonPressed(_ sender: Any) {
print(mediaPlayer.state.rawValue)
if paused {
mediaPlayer.play()
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false
if(playerDestination == .local) {
mediaPlayer.play()
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false
} else {
sendCastCommand(cmd: "Unpause")
mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
paused = false
}
} else {
mediaPlayer.pause()
mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
paused = true
if(playerDestination == .local) {
mediaPlayer.pause()
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)
}
}
//MARK: Cast start
@IBAction func castButtonPressed(_ sender: Any) {
castDeviceVC = VideoPlayerCastDeviceSelectorView()
castDeviceVC?.delegate = self
if(selectedCastDevice == nil) {
castDeviceVC = VideoPlayerCastDeviceSelectorView()
castDeviceVC?.delegate = self
castDeviceVC?.modalPresentationStyle = .popover
castDeviceVC?.popoverPresentationController?.sourceView = castButton
castDeviceVC?.modalPresentationStyle = .popover
castDeviceVC?.popoverPresentationController?.sourceView = castButton
// Present the view controller (in a popover).
self.present(castDeviceVC!, animated: true) {
print("popover visible, pause playback")
self.mediaPlayer.pause()
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
// Present the view controller (in a popover).
self.present(castDeviceVC!, animated: true) {
print("popover visible, pause playback")
self.mediaPlayer.pause()
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.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() {
optionsVC?.dismiss(animated: true, completion: nil)
self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
if(playerDestination == .local) {
self.mediaPlayer.play()
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
}
}
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
@ -221,31 +378,45 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
// Add handler for Pause Command
commandCenter.pauseCommand.addTarget { _ in
self.mediaPlayer.pause()
self.sendProgressReport(eventName: "pause")
if(self.playerDestination == .local) {
self.mediaPlayer.pause()
self.sendProgressReport(eventName: "pause")
} else {
self.sendCastCommand(cmd: "Pause")
}
self.mainActionButton.setImage(UIImage(systemName: "play"), for: .normal)
return .success
}
// Add handler for Play command
commandCenter.playCommand.addTarget { _ in
self.mediaPlayer.play()
self.sendProgressReport(eventName: "unpause")
if(self.playerDestination == .local) {
self.mediaPlayer.play()
self.sendProgressReport(eventName: "unpause")
} else {
self.sendCastCommand(cmd: "Unpause")
}
self.mainActionButton.setImage(UIImage(systemName: "pause"), for: .normal)
return .success
}
// Add handler for FF command
commandCenter.seekForwardCommand.addTarget { _ in
self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
if(self.playerDestination == .local) {
self.mediaPlayer.jumpForward(30)
self.sendProgressReport(eventName: "timeupdate")
} else {
}
return .success
}
// Add handler for RW command
commandCenter.seekBackwardCommand.addTarget { _ in
self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
if(self.playerDestination == .local) {
self.mediaPlayer.jumpBackward(15)
self.sendProgressReport(eventName: "timeupdate")
}
return .success
}
@ -255,15 +426,20 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
if let event = remoteEvent as? MPChangePlaybackPositionCommandEvent {
let targetSeconds = event.positionTime
let videoPosition = Double(self.mediaPlayer.time.intValue)
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 {
self.mediaPlayer.jumpBackward(Int32(abs(offset))/1000)
}
self.sendProgressReport(eventName: "unpause")
return .success
} else {
@ -299,11 +475,9 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
}
func mediaHasStartedPlaying() {
let scanner = CastDeviceScanner()
NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: scanner, queue: nil) { _ in
self.discoveredCastDevices = scanner.devices
if !scanner.devices.isEmpty {
NotificationCenter.default.addObserver(forName: CastDeviceScanner.deviceListDidChange, object: castScanner, queue: nil) { _ in
self.discoveredCastDevices = self.castScanner.devices
if !self.castScanner.devices.isEmpty {
self.castButton.isEnabled = true
self.castButton.setImage(UIImage(named: "CastDisconnected"), for: .normal)
} else {
@ -312,7 +486,7 @@ class PlayerViewController: UIViewController, VLCMediaDelegate, VLCMediaPlayerDe
}
}
scanner.startScanning()
castScanner.startScanning()
}
override func viewDidAppear(_ animated: Bool) {

View File

@ -43,7 +43,25 @@ struct VideoPlayerCastDeviceSelector: View {
var body: some View {
NavigationView {
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)
.navigationTitle("Select Cast Destination")