Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 1 addition & 23 deletions Sources/Navigator/Audiobook/AudioNavigator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ open class _AudioNavigator: _MediaNavigator, AudioSessionUser, Loggable {
}

// Seeks to time
let time = locator.time(forDuration: resourceDuration) ?? 0
let time = locator.locations.time?.begin ?? ((resourceDuration ?? 0) * (locator.locations.progression ?? 0))
player.seek(to: CMTime(seconds: time, preferredTimescale: 1000)) { [weak self] finished in
if let self = self, finished {
self.delegate?.navigator(self, didJumpTo: locator)
Expand Down Expand Up @@ -385,28 +385,6 @@ open class _AudioNavigator: _MediaNavigator, AudioSessionUser, Loggable {
}
}

private extension Locator {
private static let timeFragmentRegex = try! NSRegularExpression(pattern: #"t=(\d+(?:\.\d+)?)"#)

// FIXME: Should probably be in `Locator` itself.
func time(forDuration duration: Double? = nil) -> Double? {
if let progression = locations.progression, let duration = duration {
return progression * duration
} else {
for fragment in locations.fragments {
let range = NSRange(fragment.startIndex ..< fragment.endIndex, in: fragment)
if let match = Self.timeFragmentRegex.firstMatch(in: fragment, range: range) {
let matchRange = match.range(at: 1)
if matchRange.location != NSNotFound, let range = Range(matchRange, in: fragment) {
return Double(fragment[range])
}
}
}
}
return nil
}
}

private extension MediaPlaybackState {
init(_ timeControlStatus: AVPlayer.TimeControlStatus) {
switch timeControlStatus {
Expand Down
75 changes: 75 additions & 0 deletions Sources/Shared/Publication/Extensions/Audio/Locator+Audio.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
//
// Copyright 2024 Readium Foundation. All rights reserved.
// Use of this source code is governed by the BSD-style license
// available in the top-level LICENSE file of the project.
//

import Foundation

/// Audio extensions for `Locator.Locations`.
public extension Locator.Locations {
enum TimeFragment: Equatable, Sendable {
case begin(Double)
case end(Double)
case interval(Double, Double)

init?(begin: Double?, end: Double?) {
switch (begin, end) {
case let (.some(begin), .some(end)):
self = .interval(begin, end)
case let (.some(begin), .none):
self = .begin(begin)
case let (.none, .some(end)):
self = .end(end)
case (.none, .none):
return nil
}
}

public var begin: Double? {
switch self {
case let .begin(begin):
return begin
case let .interval(begin, _):
return begin
default:
return nil
}
}

public var end: Double? {
switch self {
case let .end(end):
return end
case let .interval(_, end):
return end
default:
return nil
}
}
}

private static let timeFragmentRegex = try! NSRegularExpression(pattern: #"t=([^,]*),?([^,]*)"#)

/// The Temporal Dimension media fragment, if it exists.
/// https://www.w3.org/TR/media-frags/#media-fragment-syntax
var time: TimeFragment? {
for fragment in fragments {
let range = NSRange(fragment.startIndex ..< fragment.endIndex, in: fragment)
if let match = Self.timeFragmentRegex.firstMatch(in: fragment, range: range) {
let group1NSRange = match.range(at: 1)
let group2NSRange = match.range(at: 2)
var begin: Double?
var end: Double?
if group1NSRange.location != NSNotFound, let group1Range = Range(group1NSRange, in: fragment) {
begin = Double(fragment[group1Range])
}
if group2NSRange.location != NSNotFound, let group2Range = Range(group2NSRange, in: fragment) {
end = Double(fragment[group2Range])
}
return TimeFragment(begin: begin, end: end)
}
}
return nil
}
}
Loading