mirror of
https://github.com/yattee/yattee.git
synced 2024-12-15 06:40:32 +05:30
parent
169a48e5f0
commit
2d5e34594a
@ -488,8 +488,14 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func extractStreams(from json: JSON) -> [Stream] {
|
private func extractStreams(from json: JSON) -> [Stream] {
|
||||||
extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
let hls = extractHLSStreams(from: json)
|
||||||
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue)
|
if json["liveNow"].boolValue {
|
||||||
|
return hls
|
||||||
|
}
|
||||||
|
|
||||||
|
return extractFormatStreams(from: json["formatStreams"].arrayValue) +
|
||||||
|
extractAdaptiveFormats(from: json["adaptiveFormats"].arrayValue) +
|
||||||
|
hls
|
||||||
}
|
}
|
||||||
|
|
||||||
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
private func extractFormatStreams(from streams: [JSON]) -> [Stream] {
|
||||||
@ -538,6 +544,14 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func extractHLSStreams(from content: JSON) -> [Stream] {
|
||||||
|
if let hlsURL = content.dictionaryValue["hlsUrl"]?.url {
|
||||||
|
return [Stream(hlsURL: hlsURL)]
|
||||||
|
}
|
||||||
|
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
private func extractRelated(from content: JSON) -> [Video] {
|
private func extractRelated(from content: JSON) -> [Video] {
|
||||||
content
|
content
|
||||||
.dictionaryValue["recommendedVideos"]?
|
.dictionaryValue["recommendedVideos"]?
|
||||||
@ -576,8 +590,8 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
|||||||
|
|
||||||
private func extractCaptions(from content: JSON) -> [Captions] {
|
private func extractCaptions(from content: JSON) -> [Captions] {
|
||||||
content["captions"].arrayValue.compactMap { details in
|
content["captions"].arrayValue.compactMap { details in
|
||||||
guard let baseURL = account.url,
|
let baseURL = account.url
|
||||||
let url = URL(string: baseURL + details["url"].stringValue) else { return nil }
|
guard let url = URL(string: baseURL + details["url"].stringValue) else { return nil }
|
||||||
|
|
||||||
return Captions(
|
return Captions(
|
||||||
label: details["label"].stringValue,
|
label: details["label"].stringValue,
|
||||||
|
@ -132,6 +132,8 @@ final class AVPlayerBackend: PlayerBackend {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
||||||
|
guard !model.live else { return }
|
||||||
|
|
||||||
avPlayer.seek(
|
avPlayer.seek(
|
||||||
to: time,
|
to: time,
|
||||||
toleranceBefore: .secondsInDefaultTimescale(1),
|
toleranceBefore: .secondsInDefaultTimescale(1),
|
||||||
|
@ -140,6 +140,8 @@ final class MPVClient: ObservableObject {
|
|||||||
options.append("sub-files-append=\"\(subURL)\"")
|
options.append("sub-files-append=\"\(subURL)\"")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.append("force-seekable=yes")
|
||||||
|
|
||||||
args.append(options.joined(separator: ","))
|
args.append(options.joined(separator: ","))
|
||||||
|
|
||||||
command("loadfile", args: args, returnValueCallback: completionHandler)
|
command("loadfile", args: args, returnValueCallback: completionHandler)
|
||||||
|
@ -273,7 +273,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var videoDuration: TimeInterval? {
|
var videoDuration: TimeInterval? {
|
||||||
currentItem?.duration ?? currentVideo?.length ?? playerItemDuration?.seconds
|
playerItemDuration?.seconds ?? currentItem?.duration ?? currentVideo?.length
|
||||||
}
|
}
|
||||||
|
|
||||||
var time: CMTime? {
|
var time: CMTime? {
|
||||||
@ -284,6 +284,18 @@ final class PlayerModel: ObservableObject {
|
|||||||
currentVideo?.live ?? false
|
currentVideo?.live ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var playingLive: Bool {
|
||||||
|
guard live,
|
||||||
|
let videoDuration = videoDuration,
|
||||||
|
let time = backend.currentTime?.seconds else { return false }
|
||||||
|
|
||||||
|
return videoDuration - time < 30
|
||||||
|
}
|
||||||
|
|
||||||
|
var liveStreamInAVPlayer: Bool {
|
||||||
|
live && activeBackend == .appleAVPlayer
|
||||||
|
}
|
||||||
|
|
||||||
func togglePlay() {
|
func togglePlay() {
|
||||||
backend.togglePlay()
|
backend.togglePlay()
|
||||||
}
|
}
|
||||||
@ -751,7 +763,7 @@ final class PlayerModel: ObservableObject {
|
|||||||
var nowPlayingInfo: [String: AnyObject] = [
|
var nowPlayingInfo: [String: AnyObject] = [
|
||||||
MPMediaItemPropertyTitle: video.title as AnyObject,
|
MPMediaItemPropertyTitle: video.title as AnyObject,
|
||||||
MPMediaItemPropertyArtist: video.author as AnyObject,
|
MPMediaItemPropertyArtist: video.author as AnyObject,
|
||||||
MPNowPlayingInfoPropertyIsLiveStream: video.live as AnyObject,
|
MPNowPlayingInfoPropertyIsLiveStream: live as AnyObject,
|
||||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||||
|
@ -54,6 +54,7 @@ struct ControlsOverlay: View {
|
|||||||
Text(backend.label)
|
Text(backend.label)
|
||||||
.padding(6)
|
.padding(6)
|
||||||
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
|
.foregroundColor(player.activeBackend == backend ? .accentColor : .secondary)
|
||||||
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
|
@ -317,11 +317,12 @@ struct PlayerControls: View {
|
|||||||
button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) {
|
button("Seek Backward", systemImage: "gobackward.10", size: 25, cornerRadius: 5, background: false) {
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
player.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||||
}
|
}
|
||||||
|
.disabled(player.liveStreamInAVPlayer)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.focused($focusedField, equals: .backward)
|
.focused($focusedField, equals: .backward)
|
||||||
#else
|
#else
|
||||||
.keyboardShortcut("k", modifiers: [])
|
.keyboardShortcut("k", modifiers: [])
|
||||||
.keyboardShortcut(KeyEquivalent.leftArrow, modifiers: [])
|
.keyboardShortcut(KeyEquivalent.leftArrow, modifiers: [])
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -329,11 +330,12 @@ struct PlayerControls: View {
|
|||||||
button("Seek Forward", systemImage: "goforward.10", size: 25, cornerRadius: 5, background: false) {
|
button("Seek Forward", systemImage: "goforward.10", size: 25, cornerRadius: 5, background: false) {
|
||||||
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
player.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||||
}
|
}
|
||||||
|
.disabled(player.liveStreamInAVPlayer)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.focused($focusedField, equals: .forward)
|
.focused($focusedField, equals: .forward)
|
||||||
#else
|
#else
|
||||||
.keyboardShortcut("l", modifiers: [])
|
.keyboardShortcut("l", modifiers: [])
|
||||||
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
.keyboardShortcut(KeyEquivalent.rightArrow, modifiers: [])
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -108,6 +108,7 @@ struct TimelineView: View {
|
|||||||
.animation(.easeOut, value: thumbTooltipOffset)
|
.animation(.easeOut, value: thumbTooltipOffset)
|
||||||
HStack(spacing: 4) {
|
HStack(spacing: 4) {
|
||||||
Text((dragging ? projectedValue : nil)?.formattedAsPlaybackTime(allowZero: true) ?? playerTime.currentPlaybackTime)
|
Text((dragging ? projectedValue : nil)?.formattedAsPlaybackTime(allowZero: true) ?? playerTime.currentPlaybackTime)
|
||||||
|
.opacity(player.liveStreamInAVPlayer ? 0 : 1)
|
||||||
.frame(minWidth: 35)
|
.frame(minWidth: 35)
|
||||||
#if os(tvOS)
|
#if os(tvOS)
|
||||||
.font(.system(size: 20))
|
.font(.system(size: 20))
|
||||||
@ -190,7 +191,7 @@ struct TimelineView: View {
|
|||||||
)
|
)
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
.opacity(player.liveStreamInAVPlayer ? 0 : 1)
|
||||||
.overlay(GeometryReader { proxy in
|
.overlay(GeometryReader { proxy in
|
||||||
Color.clear
|
Color.clear
|
||||||
.onAppear {
|
.onAppear {
|
||||||
@ -209,12 +210,8 @@ struct TimelineView: View {
|
|||||||
})
|
})
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
Text(dragging ? playerTime.durationPlaybackTime : playerTime.withoutSegmentsPlaybackTime)
|
durationView
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 3))
|
.frame(minWidth: 30, alignment: .trailing)
|
||||||
.frame(minWidth: 35)
|
|
||||||
#if os(tvOS)
|
|
||||||
.font(.system(size: 20))
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 3))
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
.font(.system(size: 9).monospacedDigit())
|
.font(.system(size: 9).monospacedDigit())
|
||||||
@ -222,6 +219,37 @@ struct TimelineView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder var durationView: some View {
|
||||||
|
if player.live {
|
||||||
|
if player.playingLive || player.activeBackend == .appleAVPlayer {
|
||||||
|
Text("LIVE")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding(2)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.background(RoundedRectangle(cornerRadius: 2).foregroundColor(.red))
|
||||||
|
} else {
|
||||||
|
Button {
|
||||||
|
if let duration = player.videoDuration {
|
||||||
|
player.backend.seek(to: duration - 5)
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Text("LIVE")
|
||||||
|
.fontWeight(.bold)
|
||||||
|
.padding(2)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.background(RoundedRectangle(cornerRadius: 2).strokeBorder(.red, lineWidth: 1).foregroundColor(.white))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Text(dragging ? playerTime.durationPlaybackTime : playerTime.withoutSegmentsPlaybackTime)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 3))
|
||||||
|
.frame(minWidth: 35)
|
||||||
|
#if os(tvOS)
|
||||||
|
.font(.system(size: 20))
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var tooltipVeritcalOffset: Double {
|
var tooltipVeritcalOffset: Double {
|
||||||
var offset = -20.0
|
var offset = -20.0
|
||||||
|
|
@ -1450,6 +1450,7 @@
|
|||||||
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */,
|
37A5DBC7285E371400CA4DD1 /* ControlBackgroundModifier.swift */,
|
||||||
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */,
|
37F13B61285E43C000B137E4 /* ControlsOverlay.swift */,
|
||||||
37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */,
|
37030FFE27B04DCC00ECDDAA /* PlayerControls.swift */,
|
||||||
|
37E8B0EB27B326C00024006F /* TimelineView.swift */,
|
||||||
37648B68286CF5F1003D330B /* TVControls.swift */,
|
37648B68286CF5F1003D330B /* TVControls.swift */,
|
||||||
37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */,
|
37E80F3B287B107F00561799 /* VideoDetailsOverlay.swift */,
|
||||||
);
|
);
|
||||||
@ -1499,7 +1500,6 @@
|
|||||||
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
37B81AFB26D2C9C900675966 /* VideoDetailsPaddingModifier.swift */,
|
||||||
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
37B81AF826D2C9A700675966 /* VideoPlayerSizeModifier.swift */,
|
||||||
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
37BE0BCE26A0E2D50092E2DB /* VideoPlayerView.swift */,
|
||||||
37E8B0EB27B326C00024006F /* TimelineView.swift */,
|
|
||||||
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
|
37F9619A27BD89E000058149 /* TapRecognizerViewModifier.swift */,
|
||||||
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
|
373031F22838388A000CFD59 /* PlayerLayerView.swift */,
|
||||||
);
|
);
|
||||||
|
Loading…
Reference in New Issue
Block a user