Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
… Fix Playlist PIP continuation in Background, Data URL playing, Onboarding Popup (brave/brave-ios#8550)
  • Loading branch information
Brandon-T authored Dec 18, 2023
1 parent 7521d6a commit 7601a75
Show file tree
Hide file tree
Showing 8 changed files with 243 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ extension PlaylistListViewController: UITableViewDataSource {
header.onAddPlaylist = { [unowned self] in
guard let sharedFolderUrl = folder.sharedFolderUrl else { return }

if PlayListDownloadType(rawValue: Preferences.Playlist.autoDownloadVideo.value) != nil {
if PlayListDownloadType(rawValue: Preferences.Playlist.autoDownloadVideo.value) != .off {
let controller = PopupViewController(rootView: PlaylistFolderSharingManagementView(onAddToPlaylistPressed: { [unowned self] in
self.dismiss(animated: true)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -637,7 +637,7 @@ extension PlaylistListViewController {
return
}

playerView.stop()
playerView.pause()
playerView.bringSubviewToFront(activityIndicator)
activityIndicator.startAnimating()
activityIndicator.isHidden = false
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -581,6 +581,7 @@ extension PlaylistViewController: PlaylistViewControllerDelegate {
stop(playerView)

// Cancel all loading.
PlaylistManager.shared.playbackTask?.cancel()
PlaylistManager.shared.playbackTask = nil
}

Expand Down Expand Up @@ -864,7 +865,12 @@ extension PlaylistViewController: VideoViewDelegate {
}

func load(_ videoView: VideoView, asset: AVURLAsset, autoPlayEnabled: Bool) async throws /*`MediaPlaybackError`*/ {
self.clear()
// Task will be nil if the playback has stopped, but not paused
// If it is paused, and we're loading another track, don't bother clearing the player
// as this will break PIP
if PlaylistManager.shared.playbackTask == nil {
self.clear()
}

let isNewItem = try await player.load(asset: asset)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,11 @@ public class PlaylistCarplayManager: NSObject {

func getPlaylistController(tab: Tab?, initialItem: PlaylistInfo?, initialItemPlaybackOffset: Double) -> PlaylistViewController {

// If background playback is enabled, tabs will continue to play media
// If background playback is enabled (on iPhone), tabs will continue to play media
// Even if another controller is presented and even when PIP is enabled in playlist.
// Therefore we need to stop the page/tab from playing when using playlist.
if Preferences.General.mediaAutoBackgrounding.value {
tab?.stopMediaPlayback()
}
// On iPad, media will continue to play with or without the background play setting.
tab?.stopMediaPlayback()

// If there is no media player, create one,
// pass it to the play-list controller
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ class PlaylistScriptHandler: NSObject, TabContentScript {
Self.queue.async { [weak handler] in
guard let handler = handler else { return }

if item.duration <= 0.0 && !item.detected || item.src.isEmpty || item.src.hasPrefix("data:") {
if item.duration <= 0.0 && !item.detected || item.src.isEmpty {
DispatchQueue.main.async {
handler.delegate?.updatePlaylistURLBar(tab: handler.tab, state: .none, item: nil)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,14 @@ window.__firefox__.includeOnce("Playlist", function($) {
style.visibility !== 'hidden';
}

function getAllVideoElements() {
return [...document.querySelectorAll('video')].reverse();
}

function getAllAudioElements() {
return [...document.querySelectorAll('audio')].reverse();
}

function setupLongPress() {
Object.defineProperty(window.__firefox__, '$<playlistLongPressed>', {
enumerable: false,
Expand Down Expand Up @@ -220,14 +228,6 @@ window.__firefox__.includeOnce("Playlist", function($) {
// MARK: ---------------------------------------

function setupDetector() {
function getAllVideoElements() {
return [...document.querySelectorAll('video')].reverse();
}

function getAllAudioElements() {
return [...document.querySelectorAll('audio')].reverse();
}

function requestWhenIdleShim(fn) {
var start = Date.now()
return setTimeout(function () {
Expand Down
217 changes: 215 additions & 2 deletions Sources/Playlist/PlaylistDownloadManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ public enum PlaylistDownloadError: Error {
public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
private let hlsSession: AVAssetDownloadURLSession
private let fileSession: URLSession
private let dataSession: URLSession
private let hlsDelegate = PlaylistHLSDownloadManager()
private let fileDelegate = PlaylistFileDownloadManager()
private let dataDelegate = PlaylistDataDownloadManager()
private let hlsQueue = OperationQueue.main
private let fileQueue = OperationQueue.main
private let dataQueue = OperationQueue.main

private var didRestoreSession = false
weak var delegate: PlaylistDownloadManagerDelegate?
Expand Down Expand Up @@ -68,9 +71,16 @@ public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
configuration: fileConfiguration,
delegate: fileDelegate,
delegateQueue: fileQueue)

let dataConfiguration = URLSessionConfiguration.background(withIdentifier: "com.brave.playlist.data.background.session")
dataSession = URLSession(
configuration: dataConfiguration,
delegate: dataDelegate,
delegateQueue: dataQueue)

hlsDelegate.delegate = self
fileDelegate.delegate = self
dataDelegate.delegate = self
}

func restoreSession(_ completion: @escaping () -> Void) {
Expand All @@ -92,6 +102,11 @@ public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
fileDelegate.restoreSession(fileSession) {
group.leave()
}

group.enter()
dataDelegate.restoreSession(dataSession) {
group.leave()
}

group.notify(queue: .main) {
completion()
Expand Down Expand Up @@ -119,11 +134,23 @@ public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
}
}
}

func downloadDataAsset(_ assetUrl: URL, for item: PlaylistInfo) {
if Thread.current.isMainThread {
dataDelegate.downloadAsset(self.fileSession, assetUrl: assetUrl, for: item)
} else {
fileQueue.addOperation { [weak self] in
guard let self = self else { return }
self.dataDelegate.downloadAsset(self.fileSession, assetUrl: assetUrl, for: item)
}
}
}

func cancelDownload(itemId: String) {
if Thread.current.isMainThread {
hlsDelegate.cancelDownload(itemId: itemId)
fileDelegate.cancelDownload(itemId: itemId)
dataDelegate.cancelDownload(itemId: itemId)
} else {
hlsQueue.addOperation { [weak self] in
self?.hlsDelegate.cancelDownload(itemId: itemId)
Expand All @@ -132,12 +159,16 @@ public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
fileQueue.addOperation { [weak self] in
self?.fileDelegate.cancelDownload(itemId: itemId)
}

dataQueue.addOperation { [weak self] in
self?.dataDelegate.cancelDownload(itemId: itemId)
}
}
}

func downloadTask(for itemId: String) -> MediaDownloadTask? {
if Thread.current.isMainThread {
return hlsDelegate.downloadTask(for: itemId) ?? fileDelegate.downloadTask(for: itemId)
return hlsDelegate.downloadTask(for: itemId) ?? fileDelegate.downloadTask(for: itemId) ?? dataDelegate.downloadTask(for: itemId)
}

let group = DispatchGroup()
Expand All @@ -157,9 +188,17 @@ public class PlaylistDownloadManager: PlaylistStreamDownloadManagerDelegate {
guard let self = self else { return }
fileTask = self.fileDelegate.downloadTask(for: itemId)
}

group.enter()
var dataTask: MediaDownloadTask?
dataQueue.addOperation { [weak self] in
defer { group.leave() }
guard let self = self else { return }
dataTask = self.dataDelegate.downloadTask(for: itemId)
}

group.wait()
return hlsTask ?? fileTask
return hlsTask ?? fileTask ?? dataTask
}

// MARK: - PlaylistStreamDownloadManagerDelegate
Expand Down Expand Up @@ -645,3 +684,177 @@ private class PlaylistFileDownloadManager: NSObject, URLSessionDownloadDelegate
}
}
}

private class PlaylistDataDownloadManager: NSObject, URLSessionDataDelegate {
private var activeDownloadTasks = [URLSessionTask: MediaDownloadTask]()
private var pendingCancellationTasks = [URLSessionTask]()

weak var delegate: PlaylistStreamDownloadManagerDelegate?

func restoreSession(_ session: URLSession, completion: @escaping () -> Void) {
session.getAllTasks { [weak self] tasks in
defer {
DispatchQueue.main.async {
completion()
}
}

guard let self = self else { return }

for task in tasks {
guard let itemId = task.taskDescription else {
continue
}

DispatchQueue.main.async {
if task.state != .completed,
let item = PlaylistItem.getItem(uuid: itemId),
let assetUrl = URL(string: item.mediaSrc) {
let info = PlaylistInfo(item: item)
let asset = MediaDownloadTask(id: info.tagId, name: info.name, asset: AVURLAsset(url: assetUrl, options: AVAsset.defaultOptions))
self.activeDownloadTasks[task] = asset
}
}
}
}
}

func downloadAsset(_ session: URLSession, assetUrl: URL, for item: PlaylistInfo) {
let asset = AVURLAsset(url: assetUrl, options: AVAsset.defaultOptions)

let request: URLRequest = {
var request = URLRequest(url: assetUrl, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 10.0)

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Range
request.addValue("bytes=0-", forHTTPHeaderField: "Range")
request.addValue(UUID().uuidString, forHTTPHeaderField: "X-Playback-Session-Id")
request.addValue(UserAgent.shouldUseDesktopMode ? UserAgent.desktop : UserAgent.mobile, forHTTPHeaderField: "User-Agent")
return request
}()

let task = session.dataTask(with: request)

task.taskDescription = item.tagId
activeDownloadTasks[task] = MediaDownloadTask(id: item.tagId, name: item.name, asset: asset)
task.resume()

DispatchQueue.main.async {
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: item.tagId, state: .inProgress, displayName: nil, error: nil)
}
}

func cancelDownload(itemId: String) {
if let task = activeDownloadTasks.first(where: { $0.value.id == itemId })?.key {
task.cancel() // will call didCompleteWithError which will cleanup the assets
}
}

func downloadTask(for itemId: String) -> MediaDownloadTask? {
return activeDownloadTasks.first(where: { $0.value.id == itemId })?.value
}

// MARK: - URLSessionDataDelegate

func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: Error?) {
guard let task = task as? URLSessionDownloadTask,
let asset = activeDownloadTasks.removeValue(forKey: task) else { return }

if let error = error as NSError? {
switch (error.domain, error.code) {
case (NSURLErrorDomain, NSURLErrorCancelled):
if let cacheLocation = delegate?.localAsset(for: asset.id)?.url {
do {
try FileManager.default.removeItem(at: cacheLocation)
PlaylistItem.updateCache(uuid: asset.id, cachedData: nil)
} catch {
Logger.module.error("Could not delete asset cache \(asset.name): \(error.localizedDescription)")
}
}

// Update the asset state, but do not propagate the error
// because the download was cancelled by the user
if pendingCancellationTasks.contains(task) {
pendingCancellationTasks.removeAll(where: { $0 == task })
DispatchQueue.main.async {
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: asset.id, state: .invalid, displayName: nil, error: nil)
}
return
}

case (NSURLErrorDomain, NSURLErrorUnknown):
assertionFailure("Downloading HLS streams is not supported on the simulator.")

default:
assertionFailure("An unknown error occurred while attempting to download the playlist item: \(error.domain)")
}

DispatchQueue.main.async {
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: asset.id, state: .invalid, displayName: nil, error: error)
}
}
}

func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
guard let asset = activeDownloadTasks[dataTask] else { return }

DispatchQueue.main.async {
self.delegate?.onDownloadProgressUpdate(streamDownloader: self, id: asset.id, percentComplete: 0.0)
}

func cleanupAndFailDownload(location: URL?, error: Error) {
if let location = location {
do {
try FileManager.default.removeItem(at: location)
} catch {
Logger.module.error("Error Deleting Playlist Item: \(error.localizedDescription)")
}
}

DispatchQueue.main.async {
PlaylistItem.updateCache(uuid: asset.id, cachedData: nil)
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: asset.id, state: .invalid, displayName: nil, error: error)
}
}

let path: URL? = {
do {
guard let path = try PlaylistDownloadManager.uniqueDownloadPathForFilename(asset.name + ".mp4") else {
Logger.module.error("Failed to create unique path for playlist item.")
return nil
}
return path
} catch {
return nil
}
}()

guard let path = path else {
DispatchQueue.main.async {
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: asset.id, state: .invalid, displayName: nil, error: PlaylistDownloadError.uniquePathNotCreated)
}
return
}

do {
try data.write(to: path, options: .atomic)
do {
let cachedData = try path.bookmarkData()

DispatchQueue.main.async {
PlaylistItem.updateCache(uuid: asset.id, cachedData: cachedData)
self.delegate?.onDownloadStateChanged(streamDownloader: self, id: asset.id, state: .downloaded, displayName: nil, error: nil)
}
} catch {
Logger.module.error("Failed to create bookmarkData for download URL.")
cleanupAndFailDownload(location: path, error: error)
}
} catch {
Logger.module.error("An error occurred attempting to download a playlist item: \(error.localizedDescription)")
cleanupAndFailDownload(location: path, error: error)
}

DispatchQueue.main.async {
self.delegate?.onDownloadProgressUpdate(streamDownloader: self, id: asset.id, percentComplete: 100.0)
}
}
}
7 changes: 7 additions & 0 deletions Sources/Playlist/PlaylistManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,13 @@ public class PlaylistManager: NSObject {
public func download(item: PlaylistInfo) {
guard downloadManager.downloadTask(for: item.tagId) == nil, let assetUrl = URL(string: item.src) else { return }
Task {
if assetUrl.scheme == "data" {
DispatchQueue.main.async {
self.downloadManager.downloadDataAsset(assetUrl, for: item)
}
return
}

let mimeType = await PlaylistMediaStreamer.getMimeType(assetUrl)
guard let mimeType = mimeType?.lowercased() else { return }

Expand Down

0 comments on commit 7601a75

Please sign in to comment.