1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-15 06:40:32 +05:30

Live streams fix (fix #174, #175)

This commit is contained in:
Arkadiusz Fal 2022-07-22 00:44:21 +02:00
parent 169a48e5f0
commit 2d5e34594a
8 changed files with 81 additions and 20 deletions

View File

@ -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,

View File

@ -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),

View File

@ -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)

View File

@ -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,

View File

@ -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)
} }

View File

@ -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
} }

View File

@ -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

View File

@ -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 */,
); );