Skip to content
This repository was archived by the owner on Nov 14, 2024. It is now read-only.
Draft
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
97 changes: 77 additions & 20 deletions NineAnimator/Controllers/Player Scene/AnimeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ class AnimeViewController: UITableViewController, AVPlayerViewControllerDelegate

private var animeRequestTask: NineAnimatorAsyncTask?

private var previousEpisodeRetrivalError: Error?
private var previousEpisodeRetrivalError: (Error, EpisodeLink)?

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
Expand Down Expand Up @@ -128,6 +128,20 @@ class AnimeViewController: UITableViewController, AVPlayerViewControllerDelegate
name: .playbackDidEnd,
object: nil
)

/*NotificationCenter.default.addObserver(
self,
selector: #selector(onPlaybackWillEnd(notification:)),
name: .playbackWillEnd,
object: nil
)*/

NotificationCenter.default.addObserver(
self,
selector: #selector(onAutoplayShouldPreload(notification:)),
name: .autoPlayShouldPreload,
object: nil
)
}

override func didMove(toParent parent: UIViewController?) {
Expand Down Expand Up @@ -417,13 +431,18 @@ extension AnimeViewController {

// MARK: - Initiate playback
extension AnimeViewController {
/// Retrieve the `Episode` and `PlaybackMedia` and attempt to initiate playback
private func retrieveAndPlay() {
/**
Retrieve the `Episode` and `PlaybackMedia` and attempt to initiate playback.

- Note: Must set `self.episodeLink` and `self.selectedEpisodeCell` before calling this function.

- Parameter forAutoPlay: Errors during retrieval will not be displayed immediately during autoplay. They will be displayed after playback has ended, to not interrupt the user.
*/
private func retrieveAndPlay(forAutoPlay: Bool = false) {
// Always uses self.episodeLink since it may be different from the selected cell
guard let episodeLink = episodeLink else { return }

episodeRequestTask?.cancel()
NotificationCenter.default.removeObserver(self)

let content = OfflineContentManager.shared.content(for: episodeLink)

Expand All @@ -440,9 +459,9 @@ extension AnimeViewController {
if CastController.default.isReady {
Log.info("Offline content is available, but Google Cast has been setup. Using online media.")
} else {
Log.info("Offline content is available. Using donloaded asset.")
Log.info("Offline content is available. Using downloaded asset.")
clearSelection()
onPlaybackMediaRetrieved(media)
onPlaybackMediaRetrieved(media, forAutoPlay: forAutoPlay)
return
}
}
Expand All @@ -454,7 +473,7 @@ extension AnimeViewController {
guard let episode = episode else {
if let error = error {
// `onEpisodeRetrivalStall` will make sure to unselect cell and release reference to selected cell
self.onEpisodeRetrivalStall(error, episodeLink: episodeLink)
self.onEpisodeRetrivalStall(error, episodeLink: episodeLink, retrievedForAutoPlay: forAutoPlay)
} else {
self.selectedEpisodeCell = nil
self.tableView.deselectSelectedRows()
Expand Down Expand Up @@ -486,12 +505,12 @@ extension AnimeViewController {
guard let media = media else {
guard let error = error else { return }
Log.error("Item not retrived: \"%@\"", error)
self.onPlaybackMediaStall(episode.target, error: error)
self.onPlaybackMediaStall(episode.target, error: error, retreivedForAutoPlay: forAutoPlay)
return
}

// Call media retrieved handler
self.onPlaybackMediaRetrieved(media, episode: episode)
self.onPlaybackMediaRetrieved(media, episode: episode, forAutoPlay: forAutoPlay)
}
}
} else {
Expand All @@ -500,15 +519,21 @@ extension AnimeViewController {
episode.target,
error: NineAnimatorError.providerError(
"NineAnimator does not support playing back from the selected server"
)
),
retreivedForAutoPlay: forAutoPlay
)
}
}
}
}

/// Handles an episode retrival failiure
private func onEpisodeRetrivalStall(_ error: Error, episodeLink: EpisodeLink) {
private func onEpisodeRetrivalStall(_ error: Error, episodeLink: EpisodeLink, retrievedForAutoPlay: Bool) {
guard retrievedForAutoPlay == false else {
// Save the error so it can be displayed after playback has finished
self.previousEpisodeRetrivalError = (error, episodeLink)
return
}
let restoreInterfaceElements = {
[weak self] in
self?.tableView.deselectSelectedRows()
Expand Down Expand Up @@ -635,36 +660,42 @@ extension AnimeViewController {
style: .cancel
) { _ in restoreInterfaceElements() })

previousEpisodeRetrivalError = error
previousEpisodeRetrivalError = (error, episodeLink)
present(alert, animated: true)
}

/// Handle when the link to the episode has been retrieved but no streamable link was found
private func onPlaybackMediaStall(_ fallbackURL: URL, error: Error) {
private func onPlaybackMediaStall(_ fallbackURL: URL, error: Error, retreivedForAutoPlay: Bool) {
Log.info("[PlayerViewController] Playback media retrival stalled with error: %@", error)

if NineAnimator.default.user.playbackFallbackToBrowser {
// Fallback to in-app-browser if enabled, and not using autoPlay
if NineAnimator.default.user.playbackFallbackToBrowser && !retreivedForAutoPlay {
// Cleanup selections
self.tableView.deselectSelectedRows()
self.selectedEpisodeCell = nil
let playbackController = SFSafariViewController(url: fallbackURL)
present(playbackController, animated: true)
} else if let episodeLink = episodeLink {
// Let onEpisodeRetrivalStall prompt the user for alternative options
onEpisodeRetrivalStall(error, episodeLink: episodeLink)
onEpisodeRetrivalStall(error, episodeLink: episodeLink, retrievedForAutoPlay: retreivedForAutoPlay)
}
}

/// Handle the playback media
private func onPlaybackMediaRetrieved(_ media: PlaybackMedia, episode: Episode? = nil) {
private func onPlaybackMediaRetrieved(_ media: PlaybackMedia, episode: Episode? = nil, forAutoPlay: Bool) {
// Clear previous episode error
defer { previousEpisodeRetrivalError = nil }

// Use Google Cast if it is setup and ready
if let episode = episode, CastController.default.isReady {
CastController.default.initiate(playbackMedia: media, with: episode)
CastController.default.presentPlaybackController()
} else { NativePlayerController.default.play(media: media) }
} else if forAutoPlay {
// Append Media For Autoplay
NativePlayerController.default.append(media: media)
} else {
NativePlayerController.default.play(media: media)
}
}

/// Cancels the episode retrival task
Expand Down Expand Up @@ -810,13 +841,39 @@ extension AnimeViewController {
}
}

// Update suggestion when playback did end
// Update suggestion when playback did end, and display any errors saved during autoPlay episode retrieval
@objc private func onPlaybackDidEnd(_ notification: Notification) {
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(200)) {
[weak self] in self?.tableView?.reloadSections(
[weak self] in
guard let self = self else { return }
self.tableView?.reloadSections(
Section.indexSet(.suggestion),
with: .automatic
)
if let (retrivelError, episodeLink) = self.previousEpisodeRetrivalError {
self.onEpisodeRetrivalStall(retrivelError, episodeLink: episodeLink, retrievedForAutoPlay: false)
}
}
}

// MARK: - Autoplay
// Called during the Last 2 minutes of video playback if enabled by user. Retrieves next episodeLink.
@objc private func onAutoplayShouldPreload(notification: Notification) {
// If next episode is already being requested, or the request has errored, ignore the notification
guard episodeRequestTask == nil && previousEpisodeRetrivalError == nil else { return }
// Retrieve the next EpisodeLink and it's corresponding UITableViewCell
DispatchQueue.main.async {
guard let currentEpisodeLink = NativePlayerController.default.mediaQueue.first?.link,
let currentEpisodeIndex = self.anime?.episodeLinks.firstIndex(of: currentEpisodeLink),
let nextEpisodeLink = self.anime?.episodeLink(at: currentEpisodeIndex + 1),
let episodeIndexPathForNextLink = self.indexPath(for: nextEpisodeLink),
let episodeCellForNextLink = self.tableView.cellForRow(at: episodeIndexPathForNextLink)
else { return }

// Request episode
self.selectedEpisodeCell = episodeCellForNextLink
self.episodeLink = nextEpisodeLink
self.retrieveAndPlay(forAutoPlay: true)
}
}
}
Expand Down
47 changes: 36 additions & 11 deletions NineAnimator/Controllers/Player Scene/NativePlayerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ extension NativePlayerController {
// Add observer for did reach end notification
NotificationCenter.default.addObserver(
self,
selector: #selector(onPlayerDidReachEnd(_:)),
selector: #selector(onPlayerItemDidReachEnd(_:)),
name: .AVPlayerItemDidPlayToEndTime,
object: item
)
Expand Down Expand Up @@ -315,20 +315,27 @@ extension NativePlayerController {
}
}

@objc private func onPlayerDidReachEnd(_ notification: Notification) {
@objc private func onPlayerItemDidReachEnd(_ notification: Notification) {
// Remove all did play to end time notificiation observer
NotificationCenter.default.removeObserver(
self,
name: .AVPlayerItemDidPlayToEndTime,
object: notification.object
)

DispatchQueue.main.async {
[weak self] in
guard let self = self else { return }

// Dismiss the player if no more item is in the queue
if self.mediaQueue.count == 1 {
// Remove the media from the queue, and post `playbackDidEnd` notification
Log.debug(
"[NativePlayerController] AVPlayerItem has finished playing. Removing old item from mediaQueue. New count is: %@", self.mediaQueue.count - 1)
let media = self.mediaQueue.removeFirst()
NotificationCenter.default.post(
name: .playbackDidEnd,
object: self,
userInfo: [ "media": media ]
)
// Dismiss the player if no more items are in the queue
if self.mediaQueue.isEmpty {
DispatchQueue.main.async {
[weak self] in
guard let self = self else { return }
self.playerViewController.dismiss(animated: true)
}
}
Expand All @@ -352,10 +359,28 @@ extension NativePlayerController {

// Last 15 seconds, fire will end events
if case 14.0...15.0 = currentPlaybackTMinus {
NotificationCenter.default.post(name: .playbackWillEnd, object: self, userInfo: nil)
NotificationCenter.default.post(
name: .playbackWillEnd,
object: self,
userInfo: nil
)

if player.isExternalPlaybackActive {
NotificationCenter.default.post(name: .externalPlaybackWillEnd, object: self, userInfo: nil)
NotificationCenter.default.post(
name: .externalPlaybackWillEnd,
object: self,
userInfo: nil
)
}
}
// In Last 2 minutes, if mediaQueue doesn't have more media, alert autoPlay (if enabled) to preload next media
if case 0.0...120.0 = currentPlaybackTMinus {
if mediaQueue.count <= 1 {
NotificationCenter.default.post(
name: .autoPlayShouldPreload,
object: self,
userInfo: nil
)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,9 @@ extension NASourceAniwatch {
let additionalHeaders = HTTPCookie.requestHeaderFields(with: cookies)
playbackHeaders.merge(additionalHeaders) { $1 }
}

if (Int.random(in: 0...1) == 1) {
throw NineAnimatorError.decodeError("Fake Error")
}
return Episode(
link,
target: episodeURL,
Expand Down
4 changes: 2 additions & 2 deletions NineAnimator/Models/Anime.swift
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ struct Anime {
}

/// Retrieve an episode link at index under the current server selection
func episodeLink(at index: Int) -> EpisodeLink {
episodeLinks[index]
func episodeLink(at index: Int) -> EpisodeLink? {
episodeLinks[safe: index]
}

/// Find episodes on alternative different servers with the same name
Expand Down
13 changes: 13 additions & 0 deletions NineAnimator/Utilities/Notifications.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,19 @@ extension Notification.Name {
static let playbackWillEnd =
Notification.Name("com.marcuszhou.nineanimator.playbackWillEnd")

/**
Fired within the last 2 minutes of video playback. Used to alert when the app should preload the next episode of an anime.

## Where its posted
- `NativePlayerController.persistProgress`
- Checked in `AnimeViewController`

## UserInfo
- Provides the currently playing media.
- ["currentMedia": `PlaybackMedia`]
*/
static let autoPlayShouldPreload = Notification.Name("com.marcuszhou.nineanimator.autoPlayShouldPreload")

/**
Fired after the playback has ended

Expand Down