基于本地 HTTP 代理的「边播边缓存」框架:在播放的同时把媒体数据写入本地缓存,优先用缓存数据响应播放器请求,从而减少网络流量、提升起播与播放流畅度。支持 HLS(HTTP Live Streaming) 与 基于文件的媒体(MP3 / AAC / WAV / FLAC / OGG / MP4 / MOV 等),现已完全使用 Swift 6 实现。
SJMediaCacheServer 会在本地启动一个 HTTP 代理服务器,并把原始媒体 URL 转换成指向本地代理的「播放地址」。播放器从本地代理读取数据:命中缓存时直接返回缓存内容,未命中时才向远端发起请求,同时把下载到的数据落盘缓存。
它会自动解析 HLS 播放列表(m3u8),对其中的播放列表、AES 加密 Key 以及 ts 分片分别进行代理与缓存;对基于文件的媒体则支持按 Range 分片下载与缓存。整套机制对播放器透明——你只需把播放地址换成代理地址即可。
- 代理播放请求:将原始 URL 转换为本地代理 URL,播放器从本地代理取数据,命中缓存直接返回,未命中再回源,减少网络请求、提升播放速度与可靠性。
- HLS 支持:自动解析 m3u8 播放列表,分别代理与缓存「播放列表 / AES Key / ts 分片」,支持边播边缓存。
- 基于文件的媒体支持:支持 MP3、AAC、WAV、FLAC、OGG、MP4、MOV 等常见格式,按 Range 分片缓存。
- 缓存管理:可配置缓存资源个数上限、磁盘最长保留时长、磁盘最大占用大小,以及设备预留的最小可用磁盘空间。
- 预加载(Prefetch):支持「整资源预加载」「按字节大小预加载」「按文件个数预加载(适合 HLS 提前缓存若干 ts 分片)」,并可控制最大并发预加载数。
- 资源导出(Export):将资源导出为离线可用的独立缓存,可查询状态/进度、注册观察者、统计占用大小并单独删除。导出资源与播放/预加载缓存分开管理。
- 自定义请求:可统一为下载请求追加请求头、为指定资源 URL 按数据类型设置请求头、自定义
URLSessionConfiguration,以及采集网络指标。 - 数据编解码:写入缓存前可对数据进行编码(加密),读取时再解码,便于实现本地缓存加密。
- 资源标识解析:当不同 URL 指向同一资源时,可自定义
resolveAssetIdentifier让它们共用同一份缓存(默认实现会去掉 query 部分)。 - 失败通知与日志:播放请求失败时通过通知派发;DEBUG 下可开启控制台日志并配置日志级别与选项。
- iOS 15.0+
- Swift 6(Swift 6 语言模式)
- 较新版本的 Xcode(建议使用支持 Swift 6 工具链的 Xcode)
仅支持 Swift Package Manager。
-
在 Xcode 中选择 File → Add Package Dependencies…
-
输入仓库地址:
https://github.com/moxcomic/SJMediaCacheServer.git -
依赖规则选择 Branch →
main(或后续发布的 tag),将SJMediaCacheServer库添加到你的 target。
dependencies: [
.package(url: "https://github.com/moxcomic/SJMediaCacheServer.git", branch: "main")
]并在对应 target 中声明依赖:
.target(
name: "YourApp",
dependencies: [
.product(name: "SJMediaCacheServer", package: "SJMediaCacheServer")
]
)SJMediaCacheServer 依赖以下两个 Swift Package,SPM 会自动解析,无需手动安装:
- SJUIKit(
branch: main) - CocoaAsyncSocket
把原始 URL 通过 playbackURL(with:) 转换为播放地址,再交给播放器;在播放、seek 等场景下调用 setActive(true) 确保代理服务处于激活状态。
import AVFoundation
import SJMediaCacheServer
final class PlayerController {
private let player: AVPlayer
init(url: URL) {
// 将原始 URL 转换为本地代理播放地址
let playbackURL = SJMediaCacheServer.shared.playbackURL(with: url) ?? url
player = AVPlayer(url: playbackURL)
}
func play() {
// 进入后台后, 所有连接关闭时代理服务会停止; 播放/seek 前请激活
SJMediaCacheServer.shared.setActive(true)
player.play()
}
func seek(to time: CMTime) {
SJMediaCacheServer.shared.setActive(true)
player.seek(to: time)
}
}说明:
setActive(true)启动代理服务,setActive(false)停止;可通过SJMediaCacheServer.shared.isActive查询当前是否运行。当传入的是本地文件 URL 时,playbackURL(with:)会原样返回。
let mcs = SJMediaCacheServer.shared
// 缓存资源个数上限, 0 表示不限制
mcs.cacheCountLimit = 100
// 资源在缓存中保留的最长时间(秒), 0 表示不过期
mcs.maxDiskAgeForCache = 7 * 24 * 60 * 60
// 磁盘缓存最大占用(字节), 0 表示不限制
mcs.maxDiskSizeForCache = 1024 * 1024 * 1024
// 设备预留的最小可用磁盘空间(字节), 低于该值时会清理部分缓存
mcs.reservedFreeDiskSpace = 200 * 1024 * 1024let mcs = SJMediaCacheServer.shared
// 资源是否已完整缓存
let stored = mcs.isStored(for: url)
// 可清理缓存占用大小(不含受保护缓存)
let removableBytes = mcs.countOfBytesRemovableCaches
// 删除指定资源缓存(受保护缓存不会被删除), 返回是否删除成功
let removed = mcs.removeCache(for: url)
// 删除所有未受保护的缓存
mcs.removeAllRemovableCaches()let mcs = SJMediaCacheServer.shared
// 控制最大并发预加载任务数, 默认 1
mcs.maxConcurrentPrefetchCount = 2
// 1) 预加载整个资源
let task1 = mcs.prefetch(with: url, progress: { progress in
print("prefetch progress: \(progress)")
}, completed: { error in
print("prefetch finished, error: \(String(describing: error))")
})
// 2) 按字节大小预加载(以低优先级下载)
let task2 = mcs.prefetch(with: url, preloadSize: 5 * 1024 * 1024)
// 3) 按文件个数预加载(适合 HLS 提前缓存若干 ts 分片)
let task3 = mcs.prefetch(with: url, numberOfPreloadedFiles: 3, progress: { progress in
print("HLS prefetch progress: \(progress)")
}, completed: { error in
print("HLS prefetch finished, error: \(String(describing: error))")
})
// 取消单个任务
task1?.cancel()
// 取消所有排队与执行中的预加载任务
mcs.cancelAllPrefetchTasks()导出用于把资源生成离线可用的独立缓存,与播放/预加载缓存分开管理。删除导出资源需通过导出相关接口。
let mcs = SJMediaCacheServer.shared
// 控制最大并发导出任务数, 默认 1
mcs.maxConcurrentExportCount = 1
// 获取(或创建)导出器, 并立即开始导出
let exporter = mcs.exportAsset(with: url, resumes: true)
// 查询状态与进度
let status: MCSAssetExportStatus = mcs.exportStatus(with: url) // waiting / exporting / finished / failed / suspended / cancelled
let progress: Float = mcs.exportProgress(with: url)
// 所有导出资源占用大小
let exportedBytes = mcs.countOfBytesAllExportedAssets
// 把播放产生的缓存同步进度到导出器
mcs.synchronizeForExporter(withAssetURL: url)
// 删除单个 / 全部导出资源
mcs.removeExportAsset(with: url)
mcs.removeAllExportAssets()如需监听导出事件,实现 MCSAssetExportObserver 并注册即可(无需手动注销,管理器会在合适时机自动移除):
mcs.registerExportObserver(observer)
mcs.removeExportObserver(observer)let mcs = SJMediaCacheServer.shared
// 统一为所有下载请求追加请求头
mcs.requestHandler = { request in
request.addValue("YourHeaderValue", forHTTPHeaderField: "YourHeaderField")
return request
}
// 为指定资源 URL 按数据类型设置请求头
// type 可取: .HLS / .HLSPlaylist / .HLSAESKey / .HLSTs / .FILE
mcs.assetURL(url, setValue: "YourHeaderValue", forHTTPAdditionalHeaderField: "YourHeaderField", ofType: .HLS)
// 自定义 URLSessionConfiguration
mcs.customSessionConfig { config in
config.timeoutIntervalForRequest = 15
}let mcs = SJMediaCacheServer.shared
// 写入缓存前对数据编码(加密)
mcs.writeDataEncoder = { request, offset, data in
return encrypt(data) // 你的加密实现
}
// 读取时对数据解码
mcs.readDataDecoder = { request, offset, data in
return decrypt(data) // 你的解密实现
}当不同 URL 指向同一资源时,可返回相同标识符让它们共用同一份缓存(默认实现会去掉 query 部分):
SJMediaCacheServer.shared.resolveAssetIdentifier = { url in
return url.deletingPathExtension().lastPathComponent
}// 监听播放请求失败
NotificationCenter.default.addObserver(
forName: MCSPlayBackRequestTaskDidFailedNotification,
object: nil,
queue: .main
) { note in
let url = note.userInfo?[MCSPlayBackRequestURLUserInfoKey] as? URL
let error = note.userInfo?[MCSPlayBackRequestFailureUserInfoKey] as? Error
print("playback request failed: \(String(describing: url)), \(String(describing: error))")
}
// 控制台日志(仅 DEBUG 生效)
SJMediaCacheServer.shared.isEnabledConsoleLog = true
SJMediaCacheServer.shared.logLevel = .debug本框架由 畅三江 创建并开源,本仓库为基于其作品的 Swift 6 + Swift Package Manager 迁移版本,向原作者致以诚挚感谢。
- 原作者 GitHub:changsanjiang
- Email:changsanjiang@gmail.com
- QQ 群:930508201
- KTVHTTPCache — 强大的媒体缓存框架,可缓存 HTTP 请求,非常适合媒体资源。
- RFC 7233 — HTTP Range Requests
SJMediaCacheServer 基于 MIT 许可证发布,详见仓库中的 LICENSE 文件。