mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 10:44:06 +00:00
Opening videos by URL and local files
This commit is contained in:
@@ -37,9 +37,24 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.timeControlStatus == .playing
|
||||
}
|
||||
|
||||
var videoWidth: Double? {
|
||||
if let width = avPlayer.currentItem?.presentationSize.width {
|
||||
return Double(width)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var videoHeight: Double? {
|
||||
if let height = avPlayer.currentItem?.presentationSize.height {
|
||||
return Double(height)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
#if os(iOS)
|
||||
playerLayer.videoRect.width / playerLayer.videoRect.height
|
||||
videoWidth! / videoHeight!
|
||||
#else
|
||||
VideoPlayerView.defaultAspectRatio
|
||||
#endif
|
||||
@@ -104,8 +119,17 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
preservingTime: Bool,
|
||||
upgrading _: Bool
|
||||
) {
|
||||
if let url = stream.singleAssetURL {
|
||||
if var url = stream.singleAssetURL {
|
||||
model.logger.info("playing stream with one asset\(stream.kind == .hls ? " (HLS)" : ""): \(url)")
|
||||
|
||||
if video.isLocal, video.localStreamIsFile, let localURL = video.localStream?.localURL {
|
||||
guard localURL.startAccessingSecurityScopedResource() else {
|
||||
model.navigation.presentAlert(title: "Could not open file")
|
||||
return
|
||||
}
|
||||
url = localURL
|
||||
}
|
||||
|
||||
loadSingleAsset(url, stream: stream, of: video, preservingTime: preservingTime)
|
||||
} else {
|
||||
model.logger.info("playing stream with many assets:")
|
||||
@@ -317,6 +341,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
guard video == self.model.currentVideo else {
|
||||
return
|
||||
}
|
||||
|
||||
self.avPlayer.replaceCurrentItem(with: self.model.playerItem)
|
||||
self.seekToPreservedTime { finished in
|
||||
guard finished else {
|
||||
@@ -373,7 +398,8 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
#if !os(macOS)
|
||||
var externalMetadata = [
|
||||
makeMetadataItem(.commonIdentifierTitle, value: video.title),
|
||||
makeMetadataItem(.commonIdentifierTitle, value: video.displayTitle),
|
||||
makeMetadataItem(.commonIdentifierArtist, value: video.displayAuthor),
|
||||
makeMetadataItem(.quickTimeMetadataGenre, value: video.genre ?? ""),
|
||||
makeMetadataItem(.commonIdentifierDescription, value: video.description ?? "")
|
||||
]
|
||||
|
@@ -108,6 +108,10 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.outputFps ?? 0
|
||||
}
|
||||
|
||||
var formattedOutputFps: String {
|
||||
String(format: "%.2ffps", outputFps)
|
||||
}
|
||||
|
||||
var hwDecoder: String {
|
||||
client?.hwDecoder ?? "unknown"
|
||||
}
|
||||
@@ -120,6 +124,54 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.cacheDuration ?? 0
|
||||
}
|
||||
|
||||
var videoFormat: String {
|
||||
client?.videoFormat ?? "unknown"
|
||||
}
|
||||
|
||||
var videoCodec: String {
|
||||
client?.videoCodec ?? "unknown"
|
||||
}
|
||||
|
||||
var currentVo: String {
|
||||
client?.currentVo ?? "unknown"
|
||||
}
|
||||
|
||||
var videoWidth: Double? {
|
||||
if let width = client?.width, width != "unknown" {
|
||||
return Double(width)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var videoHeight: Double? {
|
||||
if let height = client?.height, height != "unknown" {
|
||||
return Double(height)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var audioFormat: String {
|
||||
client?.audioFormat ?? "unknown"
|
||||
}
|
||||
|
||||
var audioCodec: String {
|
||||
client?.audioCodec ?? "unknown"
|
||||
}
|
||||
|
||||
var currentAo: String {
|
||||
client?.currentAo ?? "unknown"
|
||||
}
|
||||
|
||||
var audioChannels: String {
|
||||
client?.audioChannels ?? "unknown"
|
||||
}
|
||||
|
||||
var audioSampleRate: String {
|
||||
client?.audioSampleRate ?? "unknown"
|
||||
}
|
||||
|
||||
init() {
|
||||
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
self?.getTimeUpdates()
|
||||
@@ -230,6 +282,13 @@ final class MPVBackend: PlayerBackend {
|
||||
startPlaying()
|
||||
}
|
||||
|
||||
if video.isLocal, video.localStreamIsFile, let localStream = video.localStream {
|
||||
guard localStream.localURL.startAccessingSecurityScopedResource() else {
|
||||
self.model.navigation.presentAlert(title: "Could not open file")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
|
||||
self?.isLoadingVideo = true
|
||||
}
|
||||
|
@@ -198,6 +198,50 @@ final class MPVClient: ObservableObject {
|
||||
mpv.isNil ? 0.0 : getDouble("demuxer-cache-duration")
|
||||
}
|
||||
|
||||
var videoFormat: String {
|
||||
stringOrUnknown("video-format")
|
||||
}
|
||||
|
||||
var videoCodec: String {
|
||||
stringOrUnknown("video-codec")
|
||||
}
|
||||
|
||||
var currentVo: String {
|
||||
stringOrUnknown("current-vo")
|
||||
}
|
||||
|
||||
var width: String {
|
||||
stringOrUnknown("width")
|
||||
}
|
||||
|
||||
var height: String {
|
||||
stringOrUnknown("height")
|
||||
}
|
||||
|
||||
var videoBitrate: Double {
|
||||
mpv.isNil ? 0.0 : getDouble("video-bitrate")
|
||||
}
|
||||
|
||||
var audioFormat: String {
|
||||
stringOrUnknown("audio-params/format")
|
||||
}
|
||||
|
||||
var audioCodec: String {
|
||||
stringOrUnknown("audio-codec")
|
||||
}
|
||||
|
||||
var currentAo: String {
|
||||
stringOrUnknown("current-ao")
|
||||
}
|
||||
|
||||
var audioChannels: String {
|
||||
stringOrUnknown("audio-params/channels")
|
||||
}
|
||||
|
||||
var audioSampleRate: String {
|
||||
stringOrUnknown("audio-params/samplerate")
|
||||
}
|
||||
|
||||
var aspectRatio: Double {
|
||||
guard !mpv.isNil else { return VideoPlayerView.defaultAspectRatio }
|
||||
let aspect = getDouble("video-params/aspect")
|
||||
@@ -407,6 +451,10 @@ final class MPVClient: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
private func stringOrUnknown(_ name: String) -> String {
|
||||
mpv.isNil ? "unknown" : (getString(name) ?? "unknown")
|
||||
}
|
||||
|
||||
private var machine: String {
|
||||
var systeminfo = utsname()
|
||||
uname(&systeminfo)
|
||||
|
@@ -25,6 +25,9 @@ protocol PlayerBackend {
|
||||
var aspectRatio: Double { get }
|
||||
var controlsUpdates: Bool { get }
|
||||
|
||||
var videoWidth: Double? { get }
|
||||
var videoHeight: Double? { get }
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream?
|
||||
func canPlay(_ stream: Stream) -> Bool
|
||||
|
||||
|
@@ -97,6 +97,10 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||
@Published var videoBeingOpened: Video? { didSet { seek.reset() } }
|
||||
@Published var historyVideos = [Video]()
|
||||
@Published var queueItemBeingLoaded: PlayerQueueItem?
|
||||
@Published var queueItemsToLoad = [PlayerQueueItem]()
|
||||
@Published var historyItemBeingLoaded: Video.ID?
|
||||
@Published var historyItemsToLoad = [Video.ID]()
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
|
||||
@@ -373,7 +377,7 @@ final class PlayerModel: ObservableObject {
|
||||
withBackend: PlayerBackend? = nil
|
||||
) {
|
||||
playerError = nil
|
||||
if !upgrading {
|
||||
if !upgrading, !video.isLocal {
|
||||
resetSegments()
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
@@ -440,7 +444,7 @@ final class PlayerModel: ObservableObject {
|
||||
changeActiveBackend(from: activeBackend, to: backend)
|
||||
}
|
||||
|
||||
guard let stream = streamByQualityProfile else {
|
||||
guard let stream = ((availableStreams.count == 1 && availableStreams.first!.isLocal) ? availableStreams.first : nil) ?? streamByQualityProfile else {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -842,8 +846,8 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
||||
var nowPlayingInfo: [String: AnyObject] = [
|
||||
MPMediaItemPropertyTitle: video.title as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.author as AnyObject,
|
||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||
MPNowPlayingInfoPropertyIsLiveStream: live as AnyObject,
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||
@@ -952,4 +956,9 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
var formattedSize: String {
|
||||
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
|
||||
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
|
||||
}
|
||||
}
|
||||
|
@@ -68,7 +68,7 @@ extension PlayerModel {
|
||||
guard let playerInstance = self.playerInstance else { return }
|
||||
let streamsInstance = video.streams.compactMap(\.instance).first
|
||||
|
||||
if video.streams.isEmpty || streamsInstance != playerInstance {
|
||||
if !video.isLocal, video.streams.isEmpty || streamsInstance != playerInstance {
|
||||
self.loadAvailableStreams(video)
|
||||
} else {
|
||||
self.availableStreams = self.streamsWithInstance(instance: playerInstance, streams: video.streams)
|
||||
@@ -203,6 +203,7 @@ extension PlayerModel {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoDetailsLoadHandler(video, item)
|
||||
queue.insert(item, at: prepending ? 0 : queue.endIndex)
|
||||
}
|
||||
|
||||
@@ -210,11 +211,22 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
func prepareCurrentItemForHistory(finished: Bool = false) {
|
||||
if !currentItem.isNil, Defaults[.saveHistory] {
|
||||
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||
historyVideos.append(video)
|
||||
if let currentItem {
|
||||
if Defaults[.saveHistory] {
|
||||
if let video = currentVideo, !historyVideos.contains(where: { $0 == video }) {
|
||||
historyVideos.append(video)
|
||||
}
|
||||
updateWatch(finished: finished)
|
||||
}
|
||||
|
||||
if let video = currentItem.video,
|
||||
video.isLocal,
|
||||
video.localStreamIsFile,
|
||||
let localURL = video.localStream?.localURL
|
||||
{
|
||||
logger.info("stopping security scoped resource access for \(localURL)")
|
||||
localURL.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
updateWatch(finished: finished)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -253,9 +265,35 @@ extension PlayerModel {
|
||||
func loadQueueVideoDetails(_ item: PlayerQueueItem) {
|
||||
guard !accounts.current.isNil, !item.hasDetailsLoaded else { return }
|
||||
|
||||
playerAPI.loadDetails(item, completionHandler: { newItem in
|
||||
if let index = self.queue.firstIndex(where: { $0.id == item.id }) {
|
||||
self.queue[index] = newItem
|
||||
let videoID = item.video?.videoID ?? item.videoID
|
||||
|
||||
if queueItemBeingLoaded == nil {
|
||||
logger.info("loading queue details: \(videoID)")
|
||||
queueItemBeingLoaded = item
|
||||
} else {
|
||||
logger.info("POSTPONING details load: \(videoID)")
|
||||
queueItemsToLoad.append(item)
|
||||
return
|
||||
}
|
||||
|
||||
playerAPI.loadDetails(item, completionHandler: { [weak self] newItem in
|
||||
guard let self else { return }
|
||||
|
||||
self.queue.filter { $0.videoID == item.videoID }.forEach { item in
|
||||
if let index = self.queue.firstIndex(of: item) {
|
||||
self.queue[index] = newItem
|
||||
}
|
||||
}
|
||||
|
||||
self.logger.info("LOADED queue details: \(videoID)")
|
||||
|
||||
if self.queueItemBeingLoaded == item {
|
||||
self.logger.info("setting nothing loaded")
|
||||
self.queueItemBeingLoaded = nil
|
||||
}
|
||||
|
||||
if let item = self.queueItemsToLoad.popLast() {
|
||||
self.loadQueueVideoDetails(item)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@@ -42,7 +42,8 @@ struct PlayerQueueItem: Hashable, Identifiable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
var hasDetailsLoaded: Bool {
|
||||
!video.isNil
|
||||
guard let video else { return false }
|
||||
return !video.streams.isEmpty
|
||||
}
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
|
@@ -25,7 +25,13 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
}
|
||||
}
|
||||
|
||||
var localURL = ""
|
||||
if let video = value.video, video.isLocal {
|
||||
localURL = video.localStream?.localURL.absoluteString ?? ""
|
||||
}
|
||||
|
||||
return [
|
||||
"localURL": localURL,
|
||||
"videoID": value.videoID,
|
||||
"playbackTime": playbackTime,
|
||||
"videoDuration": videoDuration
|
||||
@@ -33,12 +39,7 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
}
|
||||
|
||||
func deserialize(_ object: Serializable?) -> Value? {
|
||||
guard
|
||||
let object,
|
||||
let videoID = object["videoID"]
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
guard let object else { return nil }
|
||||
|
||||
var playbackTime: CMTime?
|
||||
var videoDuration: TimeInterval?
|
||||
@@ -56,6 +57,19 @@ struct PlayerQueueItemBridge: Defaults.Bridge {
|
||||
videoDuration = TimeInterval(duration)
|
||||
}
|
||||
|
||||
if let localUrlString = object["localURL"],
|
||||
!localUrlString.isEmpty,
|
||||
let localURL = URL(string: localUrlString)
|
||||
{
|
||||
return PlayerQueueItem(
|
||||
.local(localURL),
|
||||
playbackTime: playbackTime,
|
||||
videoDuration: videoDuration
|
||||
)
|
||||
}
|
||||
|
||||
guard let videoID = object["videoID"] else { return nil }
|
||||
|
||||
return PlayerQueueItem(
|
||||
videoID: videoID,
|
||||
playbackTime: playbackTime,
|
||||
|
Reference in New Issue
Block a user