Skip to content
This repository has been archived by the owner on May 10, 2024. It is now read-only.

Fix #7485, #8012, #8151: Fix Playlist PIP continuation in Background, Data URL playing, Onboarding Popup #8550

Merged
merged 2 commits into from
Dec 18, 2023
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
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