Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support PDF outline and playback of audio playlist file, optimize the PDF scale when rotating, refactoring PreviewDocumentViewController to Swift #86

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
67 changes: 49 additions & 18 deletions MEGA.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

59 changes: 59 additions & 0 deletions MEGAUnitTests/AudioPlayer/Playlist/CUEPlaylistTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
//
// CURPlaylistTests.swift
// MEGAUnitTests
//
// Created by Meler Paine on 2023/3/16.
// Copyright © 2023 MEGA. All rights reserved.
//

import XCTest
@testable import MEGA

final class CURPlaylistTests: XCTestCase {

func testCUEParse() {
let cue = """
REM COMMENT "CUETools generated dummy CUE sheet"
FILE "01. Ascending Bird.flac" WAVE
TRACK 01 AUDIO
INDEX 01 00:00:00
FILE "02. Rusalka, Op. 114, Act 1- Song to the Moon (Arr. Diner Bennett for Cello and Orchestra).flac" WAVE
TRACK 02 AUDIO
INDEX 01 00:00:00
FILE "03. Azul- I. Paz Sulfurica.flac" WAVE
TRACK 03 AUDIO
INDEX 01 00:00:00
FILE "04. Azul- II. Silencio.flac" WAVE
TRACK 04 AUDIO
INDEX 01 00:00:00
FILE "05. Azul- III. Transit.flac" WAVE
TRACK 05 AUDIO
INDEX 01 00:00:00
FILE "06. Azul- IV. Yrushalem.flac" WAVE
TRACK 06 AUDIO
INDEX 01 00:00:00
FILE "07. Tierkreis- Leo (Arr. Shaw for Ensemble).flac" WAVE
TRACK 07 AUDIO
INDEX 01 00:00:00
FILE "08. Suite from Run Rabbit Run- I. Year of the Ox (Arr. Atkinson for Orchestra).flac" WAVE
TRACK 08 AUDIO
INDEX 01 00:00:00
FILE "09. Suite from Run Rabbit Run- II. Enjoy Your Rabbit (Arr. Atkinson for Orchestra).flac" WAVE
TRACK 09 AUDIO
INDEX 01 00:00:00
FILE "10. Suite from Run Rabbit Run- III. Year of Our Lord (Arr. Atkinson for Orchestra).flac" WAVE
TRACK 10 AUDIO
INDEX 01 00:00:00
FILE "11. Suite from Run Rabbit Run- IV. Year of the Boar (Arr. Atkinson for Orchestra).flac" WAVE
TRACK 11 AUDIO
INDEX 01 00:00:00
"""

let parser = CUEPlaylistParser(cueContent: cue)
let tracks = parser.tracks
XCTAssertEqual(tracks.count, 11)
XCTAssertEqual(tracks[0].fileName, "01. Ascending Bird.flac")
XCTAssertEqual(tracks[0].time, 0)
}

}
50 changes: 25 additions & 25 deletions iMEGA/AudioPlayer/AudioPlayer.storyboard

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,15 @@ extension AudioPlayer: AudioPlayerObservedEventsProtocol {
}

func audio(player: AVQueuePlayer, didStartPlayingCurrentItem value: NSKeyValueObservedChange<AVPlayerItem?>) {
player.pause()
if let nextPlayerItem = player.currentItem as? AudioPlayerItem {
if nextPlayerItem.startTimeStamp != nil {
self.seekPlayerItem(nextPlayerItem, to: nextPlayerItem.startTimeStamp!)
}
if isAutoPlayEnabled {
player.play()
}
}
refreshNowPlayingInfo()
}

Expand Down
4 changes: 2 additions & 2 deletions iMEGA/AudioPlayer/AudioPlayer/AudioPlayer+Notifiers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,9 @@ extension AudioPlayer: AudioPlayerNotifyObserversProtocol {
guard let player = queuePlayer else { return }

if currentNode != nil, let currentItem = currentItem() {
observer.audio?(player: player, name: currentName ?? "", artist: currentArtist ?? "", thumbnail: currentThumbnail, url: currentItem.url.absoluteString)
observer.audio?(player: player, name: currentName ?? "", artist: currentArtist ?? "", album: currentAlbum ?? "", thumbnail: currentThumbnail, url: currentItem.url.absoluteString)
} else {
observer.audio?(player: player, name: currentName ?? "", artist: currentArtist ?? "", thumbnail: currentThumbnail)
observer.audio?(player: player, name: currentName ?? "", artist: currentArtist ?? "", album: currentAlbum ?? "", thumbnail: currentThumbnail)
}
}

Expand Down
10 changes: 10 additions & 0 deletions iMEGA/AudioPlayer/AudioPlayer/AudioPlayer+PlaybackEvents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,16 @@ extension AudioPlayer: AudioPlayerStateProtocol {
@objc func playNext(_ completion: @escaping () -> Void) {
if queuePlayer?.items().count ?? 0 > 1 {
queuePlayer?.advanceToNextItem()
guard let currentItem = queuePlayer?.currentItem as? AudioPlayerItem else {
return
}
if let startTime = currentItem.startTimeStamp {
let time = CMTime(seconds: startTime, preferredTimescale: 1)
if CMTIME_IS_VALID(time) {
currentItem.seek(to: time, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
currentItem.configuredTimeOffsetFromLive = time
}
}
} else {
if queuePlayer?.items().count ?? 0 == tracks.count {
guard let currentItem = queuePlayer?.currentItem as? AudioPlayerItem else {
Expand Down
4 changes: 2 additions & 2 deletions iMEGA/AudioPlayer/AudioPlayer/AudioPlayer+Protocols.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,8 @@ protocol AudioPlayerObservedEventsProtocol {
//MARK: - Audio Player Observers Functions
@objc protocol AudioPlayerObserversProtocol: AudioPlayerProtocol {
@objc optional func audio(player: AVQueuePlayer, showLoading: Bool)
@objc optional func audio(player: AVQueuePlayer, name: String, artist: String, thumbnail: UIImage?)
@objc optional func audio(player: AVQueuePlayer, name: String, artist: String, thumbnail: UIImage?, url: String)
@objc optional func audio(player: AVQueuePlayer, name: String, artist: String, album: String, thumbnail: UIImage?)
@objc optional func audio(player: AVQueuePlayer, name: String, artist: String, album: String, thumbnail: UIImage?, url: String)
@objc optional func audio(player: AVQueuePlayer, currentItem: AudioPlayerItem?, currentThumbnail: UIImage?)
@objc optional func audio(player: AVQueuePlayer, currentItem: AudioPlayerItem?, queue: [AudioPlayerItem]?)
@objc optional func audio(player: AVQueuePlayer, currentTime: Double, remainingTime: Double, percentageCompleted: Float, isPlaying: Bool)
Expand Down
21 changes: 20 additions & 1 deletion iMEGA/AudioPlayer/AudioPlayer/AudioPlayer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,10 @@ final class AudioPlayer: NSObject {
currentItem()?.artist
}

var currentAlbum: String? {
currentItem()?.album
}

var currentThumbnail: UIImage? {
currentItem()?.artwork
}
Expand Down Expand Up @@ -151,7 +155,7 @@ final class AudioPlayer: NSObject {
setAudioPlayerSession(active: true)

queuePlayer = AVQueuePlayer(items: tracks)

queuePlayer?.actionAtItemEnd = .none
queuePlayer?.usesExternalPlaybackWhileExternalScreenIsActive = true
queuePlayer?.volume = 1.0

Expand Down Expand Up @@ -187,6 +191,13 @@ final class AudioPlayer: NSObject {
}

private func configurePlayer() {
guard let currentItem = queuePlayer?.currentItem as? AudioPlayerItem else {
return
}
if let startTime = currentItem.startTimeStamp {
seekPlayerItem(currentItem, to: startTime)
}

isAutoPlayEnabled ? play() : pause()

opQueue.cancelAllOperations()
Expand Down Expand Up @@ -300,6 +311,14 @@ final class AudioPlayer: NSObject {
tracks.compactMap{$0.url}
.contains(url)
}

func seekPlayerItem( _ playerItem: AVPlayerItem, to time: Double) {
let cmTime = CMTime(seconds: time, preferredTimescale: 1)
if CMTIME_IS_VALID(cmTime) {
playerItem.seek(to: cmTime, toleranceBefore: .zero, toleranceAfter: .zero, completionHandler: nil)
}
}

}

extension AudioPlayer: AudioPlayerTimerProtocol {
Expand Down
160 changes: 160 additions & 0 deletions iMEGA/AudioPlayer/PlaylistParser/CUEPlaylistParser.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
//
// CUEPlaylistParser.swift
// MEGA
//
// Created by Meler Paine on 2023/3/16.
// Copyright © 2023 MEGA. All rights reserved.
//

import Foundation

final class CUEPlaylistParser: NSObject {
var cueContent: String
private(set) var tracks: [CueSheetTrack] = []

init(cueContent: String) {
self.cueContent = cueContent
super.init()
tracks = parseCUEToTracks(cueContent: cueContent)
}

public func parseCUEToTracks(cueContent content: String) -> [CueSheetTrack] {
var tracks = [CueSheetTrack]()
var track: String = ""
var fileName: String = ""
var artist: String = ""
var album: String = ""
var title: String = ""
var genre: String = ""
var year: String = ""

let lines = content.split(whereSeparator: \.isNewline)
var trackAdded = false

for line in lines {
let scanner = Scanner(string: String(line))
guard let command = scanner.scanUpToCharacters(from: .whitespaces) else {
continue
}

switch command {
case "FILE":
trackAdded = false
if scanner.scanString("\"") == nil { continue }


guard let _fileName = scanner.scanUpToString("\"") else {
continue
}
fileName = _fileName
case "TRACK":
trackAdded = false
guard let _track = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
track = _track
guard let type = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
if type != "AUDIO" {
continue
}
case "PERFORMER":
guard scanner.scanString("\"") != nil else { continue }
guard let _artist = scanner.scanUpToString("\"") else { continue }
artist = _artist
case "TITLE":
guard scanner.scanString("\"") != nil else { continue }
guard let _title = scanner.scanUpToString("\"") else { continue }
if (fileName.isEmpty) {
album = _title
} else {
title = _title
}
case "REM":
guard let type = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
if type == "GENRE" {
if scanner.scanString("\"") != nil {
guard let _genre = scanner.scanUpToString("\"") else { continue }
genre = _genre
} else {
guard let _genre = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
genre = _genre
}
} else if type == "DATE" {
guard let _year = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
year = _year
}
case "INDEX":
if trackAdded || fileName.isEmpty { continue }
guard let index = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
if Int(index) != 1 { continue }

_ = scanner.scanCharacters(from: .whitespaces)
guard let time = scanner.scanUpToCharacters(from: .whitespaces) else { continue }
let timeParts = time.split(separator: ":")
if timeParts.count != 3 {
continue
}
let minute = Double(timeParts[0])!
let second = Double(timeParts[1])!
let frame = Double(timeParts[2])!
let seconds: Double = 60 * minute + second + frame / 75


if track.isEmpty {
track = "01"
}
tracks.append(CueSheetTrack(fileName: fileName, track: track, artist: artist, album: album, title: title, genre: genre, year: year, startTime: seconds))
trackAdded = true
default:
continue
}
}

// calculate track end time
if tracks.count > 1 {
for index in 1..<tracks.count {
let track = tracks[index]
let previousTrack = tracks[index - 1]
if track.fileName == previousTrack.fileName {
previousTrack.endTime = track.startTime
tracks[index - 1] = previousTrack
}
}
}

return tracks
}

func urlForPath(path: String, relativeTo baseFileUrl: URL) -> URL {
let protocolRange = (path as NSString).range(of: "://")
if (protocolRange.location != NSNotFound) {
return URL(string: path)!
}

let baseUrl = baseFileUrl.deletingLastPathComponent()
return baseUrl.appendingPathComponent(path)
}

}

final class CueSheetTrack: NSObject {
var fileName: String
var track: String
var artist: String
var album: String
var title: String
var genre: String
var year: String
var startTime: Double
var endTime: Double?

init(fileName: String, track: String, artist: String, album: String, title: String, genre: String, year: String, startTime: Double, endTime: Double? = nil) {
self.fileName = fileName
self.track = track
self.artist = artist
self.album = album
self.title = title
self.genre = genre
self.year = year
self.startTime = startTime
self.endTime = endTime
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,19 +100,20 @@ final class AudioPlayerViewController: UIViewController {
}
}

private func updateCurrentItem(name: String, artist: String, thumbnail: UIImage?, nodeSize: String?) {
private func updateCurrentItem(name: String, artist: String, album: String, thumbnail: UIImage?, nodeSize: String?) {
titleLabel.text = name
subtitleLabel.text = artist
detailLabel.text = album

if let thumbnailImage = thumbnail {
imageView.image = thumbnailImage
} else {
imageView.image = Asset.Images.AudioPlayer.defaultArtwork.image
}

if let nodeSize = nodeSize {
detailLabel.text = nodeSize
}
// if let nodeSize = nodeSize {
// detailLabel.text = nodeSize
// }
}

private func updateRepeat(_ status: RepeatMode) {
Expand Down Expand Up @@ -226,7 +227,7 @@ final class AudioPlayerViewController: UIViewController {
}

private func compactPlayer(active: Bool) {
detailLabel.isHidden = !active
// detailLabel.isHidden = !active
shuffleButton.alpha = active ? 0.0 : 1.0
repeatButton.alpha = active ? 0.0 : 1.0
gotoplaylistButton.isHidden = active
Expand Down Expand Up @@ -378,8 +379,8 @@ final class AudioPlayerViewController: UIViewController {
switch command {
case .reloadPlayerStatus(let currentTime, let remainingTime, let percentage, let isPlaying):
updatePlayerStatus(currentTime: currentTime, remainingTime: remainingTime, percentage: percentage, isPlaying: isPlaying)
case .reloadNodeInfo(let name, let artist, let thumbnail, let nodeSize):
updateCurrentItem(name: name, artist: artist, thumbnail: thumbnail, nodeSize: nodeSize)
case .reloadNodeInfo(let name, let artist, let album, let thumbnail, let nodeSize):
updateCurrentItem(name: name, artist: artist, album: album, thumbnail: thumbnail, nodeSize: nodeSize)
case .reloadThumbnail(let thumbnail):
imageView.image = thumbnail
case .showLoading(let enabled):
Expand Down
Loading