MiniPlayer/Sources/SMBStreamingLoader.swift
yumoqing b1a04526a2 feat: SMB streaming via AVAssetResourceLoaderDelegate — no file copy, reads on-demand.
- SMBStreamingLoader: wraps smb:// as miniplayer-smb:// custom scheme
- PlayerBridge.playSMBSource(): creates AVURLAsset with resource loader
- iOS NetworkFileBrowser: tap SMB media → stream directly
- Removed old FileManager.copyItem approach
2026-07-04 18:24:10 +08:00

128 lines
4.8 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Foundation
import AVFoundation
// MARK: - SMB
/// smb:// URL miniplayer-smb://
/// AVPlayer SMB
final class SMBStreamingLoader: NSObject, AVAssetResourceLoaderDelegate {
private var smbURL: URL
private var fileHandle: FileHandle?
private var contentLength: Int64 = 0
private var contentType: String = "video/mp4"
private let readChunkSize = 256 * 1024 // 256KB chunks
init(smbURL: URL) {
self.smbURL = smbURL
super.init()
// content type
let ext = smbURL.pathExtension.lowercased()
switch ext {
case "mp4", "m4v": contentType = "video/mp4"
case "mov": contentType = "video/quicktime"
case "mkv": contentType = "video/x-matroska"
case "avi": contentType = "video/x-msvideo"
case "webm": contentType = "video/webm"
case "ts": contentType = "video/mp2t"
case "m3u8": contentType = "application/vnd.apple.mpegurl"
case "mp3", "mpeg": contentType = "audio/mpeg"
case "wav": contentType = "audio/wav"
case "m4a": contentType = "audio/mp4"
case "aac": contentType = "audio/aac"
default: contentType = "video/mp4"
}
}
deinit {
try? fileHandle?.close()
}
// URL
func wrappedURL() -> URL {
// schemepath smb URL
let encoded = smbURL.absoluteString.data(using: .utf8)!.base64EncodedString()
.replacingOccurrences(of: "+", with: "-")
.replacingOccurrences(of: "/", with: "_")
.replacingOccurrences(of: "=", with: "")
return URL(string: "miniplayer-smb://stream/\(encoded)")!
}
// wrapped URL smb URL
static func decodeSMBURL(from wrapped: URL) -> URL? {
guard wrapped.scheme == "miniplayer-smb" else { return nil }
var encoded = wrapped.path.replacingOccurrences(of: "/stream/", with: "")
// base64
encoded = encoded
.replacingOccurrences(of: "-", with: "+")
.replacingOccurrences(of: "_", with: "/")
let padding = (4 - encoded.count % 4) % 4
encoded += String(repeating: "=", count: padding)
guard let data = Data(base64Encoded: encoded),
let str = String(data: data, encoding: .utf8),
let url = URL(string: str) else { return nil }
return url
}
// MARK: - AVAssetResourceLoaderDelegate
func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
shouldWaitForLoadingOfRequestedResource loadingRequest: AVAssetResourceLoadingRequest) -> Bool {
// FileHandle
if fileHandle == nil {
do {
fileHandle = try FileHandle(forReadingFrom: smbURL)
contentLength = Int64(try fileHandle!.seekToEnd())
fileHandle!.seek(toFileOffset: 0)
} catch {
loadingRequest.finishLoading(with: error)
return true
}
}
// content information request
if let infoRequest = loadingRequest.contentInformationRequest {
infoRequest.contentType = contentType
infoRequest.contentLength = contentLength
infoRequest.isByteRangeAccessSupported = true
}
// data request
if let dataRequest = loadingRequest.dataRequest {
let offset = dataRequest.currentOffset > 0
? dataRequest.currentOffset
: (dataRequest.requestedOffset > 0 ? dataRequest.requestedOffset : 0)
let length = dataRequest.requestedLength > 0
? dataRequest.requestedLength
: readChunkSize
do {
try fileHandle?.seek(toOffset: UInt64(offset))
let data = fileHandle?.readData(ofLength: length) ?? Data()
if !data.isEmpty {
dataRequest.respond(with: data)
loadingRequest.finishLoading()
} else {
//
loadingRequest.finishLoading()
}
} catch {
loadingRequest.finishLoading(with: error)
}
} else {
// data request info request
loadingRequest.finishLoading()
}
return true
}
func resourceLoader(_ resourceLoader: AVAssetResourceLoader,
didCancel loadingRequest: AVAssetResourceLoadingRequest) {
//
}
}