Skip to content

Commit

Permalink
Support opening and end credits (#850)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Défago <[email protected]>
  • Loading branch information
waliid and defagos authored Apr 26, 2024
1 parent fda8dae commit 78c0fda
Show file tree
Hide file tree
Showing 22 changed files with 336 additions and 23 deletions.
3 changes: 3 additions & 0 deletions Demo/Resources/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,9 @@
},
"Simulate memory warning" : {

},
"Skip" : {

},
"Smart navigation" : {

Expand Down
3 changes: 2 additions & 1 deletion Demo/Sources/Examples/ExamplesViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ final class ExamplesViewModel: ObservableObject {
URLTemplate.dvrVideoHLS,
URLTemplate.liveTimestampVideoHLS,
URLTemplate.onDemandAudioMP3,
URLTemplate.liveAudioMP3
URLTemplate.liveAudioMP3,
URLTemplate.timeRangesVideo
])

let urnMedias = Template.medias(from: [
Expand Down
12 changes: 8 additions & 4 deletions Demo/Sources/Model/Media.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ struct Media: Hashable {
let type: `Type`
let isMonoscopic: Bool
let startTime: CMTime
let timeRanges: [TimeRange]

init(
title: String,
Expand All @@ -38,7 +39,8 @@ struct Media: Hashable {
image: UIImage? = nil,
type: `Type`,
isMonoscopic: Bool = false,
startTime: CMTime = .zero
startTime: CMTime = .zero,
timeRanges: [TimeRange] = []
) {
self.title = title
self.subtitle = subtitle
Expand All @@ -47,6 +49,7 @@ struct Media: Hashable {
self.type = type
self.isMonoscopic = isMonoscopic
self.startTime = startTime
self.timeRanges = timeRanges
}

init(from template: Template, startTime: CMTime = .zero) {
Expand All @@ -56,7 +59,8 @@ struct Media: Hashable {
imageUrl: template.imageUrl,
type: template.type,
isMonoscopic: template.isMonoscopic,
startTime: startTime
startTime: startTime,
timeRanges: template.timeRanges
)
}

Expand Down Expand Up @@ -91,7 +95,7 @@ extension Media {
.map { image in
.simple(
url: url,
metadata: Media(title: title, subtitle: subtitle, image: image, type: type),
metadata: Media(title: title, subtitle: subtitle, image: image, type: type, timeRanges: timeRanges),
configuration: configuration
)
},
Expand All @@ -115,6 +119,6 @@ extension Media {

extension Media: AssetMetadata {
var playerMetadata: PlayerMetadata {
.init(title: title, subtitle: subtitle, image: image)
.init(title: title, subtitle: subtitle, image: image, timeRanges: timeRanges)
}
}
22 changes: 21 additions & 1 deletion Demo/Sources/Model/Template.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//

import AVFoundation
import PillarboxPlayer

private let kAppleImageUrl = URL("https://www.apple.com/newsroom/images/default/apple-logo-og.jpg?202312141200")
private let kBitmovinImageUrl = URL("""
Expand Down Expand Up @@ -65,6 +66,16 @@ enum URLTemplate {
imageUrl: "https://img.rts.ch/articles/2017/image/cxsqgp-25867841.image?w=640&h=640",
type: .url("http://stream.srg-ssr.ch/m/couleur3/mp3_128")
)
static let timeRangesVideo = Template(
title: "Bip",
subtitle: "Content with opening and closing credits",
imageUrl: "https://www.rts.ch/2023/05/01/10/22/10253916.image/16x9",
type: .url("https://rts-vod-amd.akamaized.net/ch/13986102/d13bcd9d-7030-3f5a-b28c-f9abfa6795b8/master.m3u8"),
timeRanges: [
.init(kind: .credits(.opening), start: .init(value: 3, timescale: 1), end: .init(value: 7, timescale: 1)),
.init(kind: .credits(.closing), start: .init(value: 163, timescale: 1), end: .init(value: 183_680, timescale: 1000))
]
)
static let appleBasic_4_3_HLS = Template(
title: "Apple Basic 4:3",
subtitle: "4x3 aspect ratio, H.264 @ 30Hz",
Expand Down Expand Up @@ -321,13 +332,22 @@ struct Template: Hashable {
let imageUrl: URL?
let type: Media.`Type`
let isMonoscopic: Bool
let timeRanges: [TimeRange]

init(title: String, subtitle: String? = nil, imageUrl: URL? = nil, type: Media.`Type`, isMonoscopic: Bool = false) {
init(
title: String,
subtitle: String? = nil,
imageUrl: URL? = nil,
type: Media.`Type`,
isMonoscopic: Bool = false,
timeRanges: [TimeRange] = []
) {
self.title = title
self.subtitle = subtitle
self.imageUrl = imageUrl
self.type = type
self.isMonoscopic = isMonoscopic
self.timeRanges = timeRanges
}

static func medias(from templates: [Self]) -> [Media] {
Expand Down
111 changes: 100 additions & 11 deletions Demo/Sources/Players/PlaybackView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,10 @@ private struct MainView: View {
@Binding var layout: PlaybackView.Layout
let isMonoscopic: Bool
let supportsPictureInPicture: Bool
let progressTracker: ProgressTracker

@StateObject private var visibilityTracker = VisibilityTracker()

@State private var progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 1))
@State private var layoutInfo: LayoutInfo = .none
@State private var selectedGravity: AVLayerVideoGravity = .resizeAspect
@State private var isInteracting = false
Expand All @@ -42,8 +42,6 @@ private struct MainView: View {
.statusBarHidden(isFullScreen ? isUserInterfaceHidden : false)
.animation(.defaultLinear, value: isUserInterfaceHidden)
.bind(visibilityTracker, to: player)
.bind(progressTracker, to: player)
._debugBodyCounter()
}

private var isFullScreen: Bool {
Expand Down Expand Up @@ -122,13 +120,26 @@ private struct MainView: View {

@ViewBuilder
private func bottomBar() -> some View {
VStack(spacing: 20) {
skipButton()
bottomControls()
}
.animation(.linear(duration: 0.2), values: isUserInterfaceHidden, isInteracting)
.padding(.horizontal)
.padding(.vertical, 10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}

@ViewBuilder
private func bottomControls() -> some View {
VStack(spacing: 0) {
HStack(alignment: .bottom) {
metadata()
if isFullScreen {
bottomButtons()
}
}

HStack(spacing: 20) {
TimeBar(player: player, visibilityTracker: visibilityTracker, isInteracting: $isInteracting)
if !isFullScreen {
Expand All @@ -138,10 +149,6 @@ private struct MainView: View {
}
.preventsTouchPropagation()
.opacity(isUserInterfaceHidden ? 0 : 1)
.animation(.linear(duration: 0.2), values: isUserInterfaceHidden, isInteracting)
.padding(.horizontal)
.padding(.vertical, 10)
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
}

@ViewBuilder
Expand Down Expand Up @@ -225,6 +232,13 @@ private struct MainView: View {
}
}

@ViewBuilder
private func skipButton() -> some View {
SkipButton(player: player, progressTacker: progressTracker)
.padding(.trailing, 20)
.frame(maxWidth: .infinity, alignment: .trailing)
}

@ViewBuilder
private func image(name: String) -> some View {
Image(systemName: name)
Expand All @@ -238,6 +252,39 @@ private struct MainView: View {
}
}

private struct SkipButton: View {
let player: Player
@ObservedObject var progressTacker: ProgressTracker

private var skippableTimeRange: TimeRange? {
player.skippableTimeRange(at: progressTacker.time)
}

var body: some View {
Button(action: skip) {
Text("Skip")
.font(.footnote)
.foregroundStyle(.white)
.padding(.vertical, 5)
.padding(.horizontal, 10)
.background {
RoundedRectangle(cornerRadius: 2)
.fill(Color(uiColor: UIColor.darkGray))
RoundedRectangle(cornerRadius: 2)
.stroke(lineWidth: 2.0)
.foregroundStyle(.gray)
}
}
.opacity(skippableTimeRange != nil ? 1 : 0)
.animation(.easeInOut, value: skippableTimeRange)
}

private func skip() {
guard let skippableTimeRange else { return }
player.seek(to: skippableTimeRange.end)
}
}

private struct ControlsView: View {
@ObservedObject var player: Player
@ObservedObject var progressTracker: ProgressTracker
Expand Down Expand Up @@ -526,6 +573,34 @@ private struct TimeSlider: View {
}
}

#else

private struct MainSystemView: View {
let player: Player
let supportsPictureInPicture: Bool
@ObservedObject var progressTracker: ProgressTracker

private var contextualActions: [ContextualAction] {
if let skippableTimeRange = player.skippableTimeRange(at: progressTracker.time) {
return [
.init(title: "Skip") {
player.seek(to: skippableTimeRange.end)
}
]
}
else {
return []
}
}

var body: some View {
SystemVideoView(player: player)
.supportsPictureInPicture(supportsPictureInPicture)
.contextualActions(contextualActions)
.ignoresSafeArea()
}
}

#endif

// Behavior: h-hug, v-hug
Expand Down Expand Up @@ -609,6 +684,7 @@ struct PlaybackView: View {

@ObservedObject private var player: Player
@Binding private var layout: Layout
@State private var progressTracker = ProgressTracker(interval: CMTime(value: 1, timescale: 1))

private var isMonoscopic = false
private var supportsPictureInPicture = false
Expand All @@ -632,6 +708,7 @@ struct PlaybackView: View {
}
}
.background(.black)
.bind(progressTracker, to: player)
}

init(player: Player, layout: Binding<Layout> = .constant(.inline)) {
Expand All @@ -647,7 +724,8 @@ struct PlaybackView: View {
player: player,
layout: $layout,
isMonoscopic: isMonoscopic,
supportsPictureInPicture: supportsPictureInPicture
supportsPictureInPicture: supportsPictureInPicture,
progressTracker: progressTracker
)
#else
if isMonoscopic {
Expand All @@ -656,12 +734,15 @@ struct PlaybackView: View {
.ignoresSafeArea()
}
else {
SystemVideoView(player: player)
.supportsPictureInPicture(supportsPictureInPicture)
.ignoresSafeArea()
MainSystemView(
player: player,
supportsPictureInPicture: supportsPictureInPicture,
progressTracker: progressTracker
)
}
#endif
}
._debugBodyCounter()
}
}

Expand All @@ -686,6 +767,14 @@ private extension View {
}
}

private extension Player {
func skippableTimeRange(at time: CMTime) -> TimeRange? {
metadata.timeRanges.first { timeRange in
timeRange.containsTime(time)
}
}
}

#Preview {
PlaybackView(player: Player(item: Media(from: URLTemplate.onDemandVideoLocalHLS).playerItem()))
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ public extension MediaComposition {
case _segments = "segmentList"
case _markIn = "fullLengthMarkIn"
case _markOut = "fullLengthMarkOut"
case _timeIntervals = "timeIntervalList"
case blockingReason = "blockReason"
case contentType = "type"
case date
Expand Down Expand Up @@ -77,6 +78,11 @@ public extension MediaComposition {
_analyticsMetadata ?? [:]
}

/// The time interval associated with the chapter.
public var timeIntervals: [TimeInterval] {
_timeIntervals ?? []
}

/// Time range associated with the chapter.
public var timeRange: CMTimeRange {
guard let _markIn, let _markOut else { return .zero }
Expand All @@ -98,6 +104,9 @@ public extension MediaComposition {
// swiftlint:disable:next discouraged_optional_collection
private let _resources: [Resource]?

// swiftlint:disable:next discouraged_optional_collection
private let _timeIntervals: [TimeInterval]?

private let _markIn: Int64?
private let _markOut: Int64?
}
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loading

0 comments on commit 78c0fda

Please sign in to comment.