{"id":260,"date":"2025-11-29T08:56:08","date_gmt":"2025-11-29T00:56:08","guid":{"rendered":"https:\/\/www.52runoob.com\/?p=260"},"modified":"2025-11-29T08:56:08","modified_gmt":"2025-11-29T00:56:08","slug":"avplayer-%e6%92%ad%e6%94%be%e5%9c%a8%e7%ba%bf%e8%a7%86%e9%a2%91%e5%92%8c%e6%9c%ac%e5%9c%b0%e6%96%87%e4%bb%b6%e6%95%b0%e6%8d%ae%e5%ba%93%e8%ae%be%e7%bd%ae","status":"publish","type":"post","link":"https:\/\/www.52runoob.com\/index.php\/2025\/11\/29\/avplayer-%e6%92%ad%e6%94%be%e5%9c%a8%e7%ba%bf%e8%a7%86%e9%a2%91%e5%92%8c%e6%9c%ac%e5%9c%b0%e6%96%87%e4%bb%b6%e6%95%b0%e6%8d%ae%e5%ba%93%e8%ae%be%e7%bd%ae\/","title":{"rendered":"AVPlayer \u64ad\u653e\u5728\u7ebf\u89c6\u9891\u548c\u672c\u5730\u6587\u4ef6(\u6570\u636e\u5e93)\u8bbe\u7f6e"},"content":{"rendered":"\n<p>\u4e0b\u9762\u7ed9\u4f60\u4e00\u4efd<strong>\u8d85\u6e05\u6670\u3001\u53ef\u76f4\u63a5\u4e0a\u624b\u7684 AVPlayer \u64ad\u653e\u201c\u5728\u7ebf\u89c6\u9891 + \u672c\u5730\u6587\u4ef6\uff08\u542b\u6570\u636e\u5e93\u8def\u5f84\u7ba1\u7406\uff09\u201d\u7684\u5b8c\u6574\u65b9\u6848<\/strong>\uff0c\u5305\u62ec\u5e38\u89c1\u8e29\u5751\u70b9\u3001\u6700\u4f73\u5b9e\u8df5\u3001\u793a\u4f8b\u4ee3\u7801\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u2705 <strong>\u4e00\u3001\u64ad\u653e\u5728\u7ebf\u89c6\u9891\uff08URL\uff09<\/strong><\/h1>\n\n\n\n<p>AVPlayer \u64ad\u653e\u5728\u7ebf\u89c6\u9891\u6700\u7b80\u5355\uff0c\u53ea\u9700\u8981\u7ed9\u5b83\u4e00\u4e2a\u8fdc\u7a0b URL\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>\u793a\u4f8b\u4ee3\u7801\uff1a<\/strong><\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport AVFoundation\nimport AVKit\n\nfunc playOnlineVideo(urlString: String, on vc: UIViewController) {\n    guard let url = URL(string: urlString) else { return }\n    \n    let player = AVPlayer(url: url)\n    let playerVC = AVPlayerViewController()\n    playerVC.player = player\n    \n    vc.present(playerVC, animated: true) {\n        player.play()\n    }\n}\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\"><strong>\u6ce8\u610f\u70b9\uff1a<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>\u9700\u8981 HTTPS \u6216 ATS \u653e\u5f00\u3002<\/li>\n\n\n\n<li>\u907f\u514d URL \u88ab\u7f13\u5b58\uff0c\u53ef\u4ee5\u52a0\u968f\u673a\u53c2\u6570 <code>?t=timestamp<\/code>\u3002<\/li>\n\n\n\n<li>\u82e5\u9700\u8981\u9884\u52a0\u8f7d\uff0c\u53ef\u7528 <code>AVPlayerItem<\/code> + KVO\u3002<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u2705 <strong>\u4e8c\u3001\u64ad\u653e\u672c\u5730\u89c6\u9891\uff08\u6587\u4ef6\u8def\u5f84\uff09<\/strong><\/h1>\n\n\n\n<p>\u4f60\u7684\u89c6\u9891\u82e5\u5b58\u5230\u6c99\u76d2\uff0c\u5982 <strong>Documents \/ tmp \/ Caches<\/strong>\uff0c\u53ef\u4ee5\u76f4\u63a5\u7528 File URL \u64ad\u653e\u3002<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><strong>\u793a\u4f8b\uff1a<\/strong><\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfunc playLocalVideo(filePath: String, on vc: UIViewController) {\n    let url = URL(fileURLWithPath: filePath)\n    \n    let player = AVPlayer(url: url)\n    let playerVC = AVPlayerViewController()\n    playerVC.player = player\n    \n    vc.present(playerVC, animated: true) {\n        player.play()\n    }\n}\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\">\u6c99\u76d2\u6587\u4ef6\u8def\u5f84\u793a\u4f8b\uff1a<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nlet path = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!\nlet filePath = path + &quot;\/myVideo.mp4&quot;\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u2705 <strong>\u4e09\u3001\u89c6\u9891\u4fe1\u606f\u5b58\u5230\u6570\u636e\u5e93\uff08SQLite \/ CoreData\uff09<\/strong><\/h1>\n\n\n\n<p>\u89c6\u9891\u672c\u8eab <strong>\u4e0d\u5efa\u8bae\u5b58\u8fdb\u6570\u636e\u5e93<\/strong>\uff08\u4f53\u79ef\u592a\u5927\u3001IO\u5f00\u9500\u5de8\u5927\uff09\uff0c\u901a\u5e38\u5b58\uff1a<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th>\u5b57\u6bb5<\/th><th>\u5185\u5bb9<\/th><\/tr><\/thead><tbody><tr><td>id<\/td><td>\u4e3b\u952e<\/td><\/tr><tr><td>title<\/td><td>\u89c6\u9891\u540d\u79f0<\/td><\/tr><tr><td>filePath<\/td><td>\u672c\u5730\u6587\u4ef6\u7edd\u5bf9\u8def\u5f84<\/td><\/tr><tr><td>remoteURL<\/td><td>\u5728\u7ebf\u89c6\u9891 URL<\/td><\/tr><tr><td>duration<\/td><td>\u65f6\u957f<\/td><\/tr><tr><td>cover<\/td><td>\u5c01\u9762\u56fe\uff08\u53ef\u5b58 URL \u6216 Base64\uff09<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">SQLite \u8868\u7ed3\u6784\u793a\u4f8b\uff1a<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nCREATE TABLE video (\n  id INTEGER PRIMARY KEY AUTOINCREMENT,\n  title TEXT,\n  filePath TEXT,\n  remoteURL TEXT,\n  duration REAL,\n  cover TEXT\n);\n\n<\/pre><\/div>\n\n\n<h3 class=\"wp-block-heading\">\u4fdd\u5b58\u793a\u4f8b\uff08Swift\uff09\uff1a<\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nstruct VideoModel {\n    var id: Int\n    var title: String\n    var filePath: String?\n    var remoteURL: String?\n}\n\n<\/pre><\/div>\n\n\n<p>\u6570\u636e\u5e93\u50a8\u5b58\u540e\uff0c\u901a\u8fc7 <code>filePath<\/code> \u6216 <code>remoteURL<\/code> \u6765\u5224\u65ad\u64ad\u653e\u6a21\u5f0f\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u2705 <strong>\u56db\u3001\u4ece\u6570\u636e\u5e93\u8bfb\u53d6\u5e76\u5224\u65ad\u201c\u64ad\u653e\u672c\u5730 or \u5728\u7ebf\u201d<\/strong><\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nfunc playVideo(model: VideoModel, on vc: UIViewController) {\n    if let path = model.filePath, FileManager.default.fileExists(atPath: path) {\n        \/\/ \u672c\u5730\u5b58\u5728 \u2192 \u64ad\u672c\u5730\n        playLocalVideo(filePath: path, on: vc)\n    } else if let urlString = model.remoteURL {\n        \/\/ \u64ad\u5728\u7ebf\u89c6\u9891\n        playOnlineVideo(urlString: urlString, on: vc)\n    } else {\n        print(&quot;\u65e0\u53ef\u64ad\u653e\u8d44\u6e90&quot;)\n    }\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\u2705 <strong>\u4e94\u3001\u9644\u52a0\u529f\u80fd\uff08\u53ef\u9009\uff09<\/strong><\/h1>\n\n\n\n<h3 class=\"wp-block-heading\">1. <strong>\u64ad\u653e\u8fdb\u5ea6\u5b58\u50a8\u6570\u636e\u5e93<\/strong><\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nplayer.addPeriodicTimeObserver(\n    forInterval: CMTime(seconds: 1, preferredTimescale: 1),\n    queue: .main\n) { &#x5B;weak self] time in\n    let seconds = CMTimeGetSeconds(time)\n    \/\/ \u5199\u5165\u6570\u636e\u5e93 update video set progress = seconds\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">2. <strong>\u7f13\u5b58\u5728\u7ebf\u89c6\u9891\u5230\u672c\u5730\uff08\u79bb\u7ebf\u64ad\u653e\uff09<\/strong><\/h3>\n\n\n\n<p>\u4f60\u53ef\u4ee5\u4f7f\u7528\uff1a<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>URLSessionDownloadTask<\/code><\/li>\n\n\n\n<li>\u6216\u4e09\u65b9\u5e93 <strong>KTVHTTPCache<\/strong>\uff08\u6700\u5f3a\u7f13\u5b58\u65b9\u6848\uff09<\/li>\n<\/ul>\n\n\n\n<p>\u4e0b\u8f7d\u6210\u529f \u2192 \u4fdd\u5b58\u5230\u6c99\u76d2 \u2192 \u5199\u5165\u6570\u636e\u5e93 filePath\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h3 class=\"wp-block-heading\">3. <strong>\u4f7f\u7528 AVAsset \u9884\u52a0\u8f7d\u89c6\u9891\u4fe1\u606f<\/strong><\/h3>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nlet asset = AVURLAsset(url: url)\nasset.loadValuesAsynchronously(forKeys: &#x5B;&quot;duration&quot;]) {\n    var error: NSError?\n    let status = asset.statusOfValue(forKey: &quot;duration&quot;, error: &amp;amp;error)\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udce6 <strong>\u516d\u3001\u53ef\u76f4\u63a5\u5957\u7528\u7684\u5b8c\u6574\u6d41\u7a0b\u56fe<\/strong><\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\n            \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n            \u2502   \u6570\u636e\u5e93     \u2502\n            \u2502 video table  \u2502\n            \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n                   \u2502\u8bfb\u53d6\u89c6\u9891\u4fe1\u606f\n                   \u25bc\n        \u672c\u5730 filePath \u5b58\u5728\uff1f \u2500\u2500\u2500\u2500\u2500\u2500\u25ba \u662f\uff1a\u64ad\u653e\u672c\u5730\u89c6\u9891\n                   \u2502\u5426\n                   \u25bc\n            remoteURL \u5b58\u5728\uff1f\n                   \u2502\n            \u662f\uff1a\u64ad\u653e\u5728\u7ebf\u89c6\u9891\n\n<\/pre><\/div>\n\n\n<p>\u597d\u7684\u963f\u6770\uff0c\u6211\u7ed9\u4f60\u51c6\u5907\u4e00\u5957 <strong>\u771f\u6b63\u53ef\u76f4\u63a5\u96c6\u6210\u5230 App \u7684\u5b8c\u6574\u89c6\u9891\u64ad\u653e\u6a21\u5757\u4ee3\u7801<\/strong>\uff1a<br>\u5305\u542b <strong>\u5728\u7ebf\u64ad\u653e + \u672c\u5730\u64ad\u653e + SQLite \u6570\u636e\u5e93\u5b58\u50a8 + \u4e0b\u8f7d\u7f13\u5b58 + \u64ad\u653e\u7ba1\u7406\u5668\u5c01\u88c5 + \u901a\u7528\u64ad\u653e\u5668\u754c\u9762<\/strong>\u3002<br>\u4f60\u53ea\u9700\u8981\u62d6\u8fdb\u9879\u76ee\u5373\u53ef\u8fd0\u884c\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udce6 <strong>AVPlayer \u5b8c\u6574\u6a21\u5757\uff08\u5168\u5957\u4ee3\u7801\uff09<\/strong><\/h1>\n\n\n\n<p>\u4e0b\u9762\u6211\u4f1a\u6309\u6a21\u5757\u62c6\u5206\uff0c\u4f60\u53ef\u4ee5\u76f4\u63a5\u590d\u5236\u6bcf\u4e2a\u6587\u4ef6\u5230\u9879\u76ee\u4e2d\u5373\u53ef\u3002<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">1\ufe0f\u20e3 <strong>VideoModel.swift \u2014 \u89c6\u9891\u6570\u636e\u6a21\u578b<\/strong><\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport Foundation\n\nstruct VideoModel: Codable {\n    var id: Int64?\n    var title: String\n    var filePath: String?     \/\/ \u672c\u5730\u5b58\u50a8\u8def\u5f84\n    var remoteURL: String?    \/\/ \u5728\u7ebf URL\n    var progress: Double?     \/\/ \u64ad\u653e\u8fdb\u5ea6\n    var duration: Double?     \/\/ \u89c6\u9891\u65f6\u957f\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">2\ufe0f\u20e3 <strong>VideoDatabase.swift \u2014 SQLite \u6570\u636e\u5e93\u6a21\u5757\uff08\u53ef\u76f4\u63a5\u7528\uff09<\/strong><\/h1>\n\n\n\n<p>\u4f7f\u7528 <code>SQLite3<\/code> \u539f\u751f\u5e93\uff0c\u4e0d\u4f9d\u8d56\u7b2c\u4e09\u65b9\u3002<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport Foundation\nimport SQLite3\n\nclass VideoDatabase {\n    static let shared = VideoDatabase()\n    private let dbName = &quot;video.db&quot;\n    private var db: OpaquePointer?\n\n    private init() {\n        openDB()\n        createTable()\n    }\n\n    private func openDB() {\n        let doc = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!\n        let path = doc + &quot;\/\\(dbName)&quot;\n\n        if sqlite3_open(path, &amp;amp;db) != SQLITE_OK {\n            print(&quot;\u274c \u6570\u636e\u5e93\u6253\u5f00\u5931\u8d25&quot;)\n        }\n    }\n\n    private func createTable() {\n        let sql = &quot;&quot;&quot;\n        CREATE TABLE IF NOT EXISTS video (\n            id INTEGER PRIMARY KEY AUTOINCREMENT,\n            title TEXT,\n            filePath TEXT,\n            remoteURL TEXT,\n            progress REAL,\n            duration REAL\n        );\n        &quot;&quot;&quot;\n\n        var stmt: OpaquePointer?\n        sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil)\n        sqlite3_step(stmt)\n        sqlite3_finalize(stmt)\n    }\n\n    \/\/ \u63d2\u5165\n    func insertVideo(_ model: VideoModel) {\n        let sql = &quot;INSERT INTO video (title, filePath, remoteURL, progress, duration) VALUES (?,?,?,?,?)&quot;\n        var stmt: OpaquePointer?\n\n        sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil)\n        sqlite3_bind_text(stmt, 1, model.title, -1, nil)\n        sqlite3_bind_text(stmt, 2, (model.filePath ?? &quot;&quot;), -1, nil)\n        sqlite3_bind_text(stmt, 3, (model.remoteURL ?? &quot;&quot;), -1, nil)\n        sqlite3_bind_double(stmt, 4, model.progress ?? 0)\n        sqlite3_bind_double(stmt, 5, model.duration ?? 0)\n\n        sqlite3_step(stmt)\n        sqlite3_finalize(stmt)\n    }\n\n    \/\/ \u67e5\u8be2\u5168\u90e8\n    func queryAll() -&gt; &#x5B;VideoModel] {\n        let sql = &quot;SELECT id,title,filePath,remoteURL,progress,duration FROM video&quot;\n        var stmt: OpaquePointer?\n        sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil)\n\n        var list = &#x5B;VideoModel]()\n\n        while sqlite3_step(stmt) == SQLITE_ROW {\n            let id = sqlite3_column_int64(stmt, 0)\n            let title = String(cString: sqlite3_column_text(stmt, 1))\n            let filePath = String(cString: sqlite3_column_text(stmt, 2))\n            let remoteURL = String(cString: sqlite3_column_text(stmt, 3))\n            let progress = sqlite3_column_double(stmt, 4)\n            let duration = sqlite3_column_double(stmt, 5)\n\n            list.append(\n                VideoModel(id: id,\n                           title: title,\n                           filePath: filePath.isEmpty ? nil : filePath,\n                           remoteURL: remoteURL.isEmpty ? nil : remoteURL,\n                           progress: progress,\n                           duration: duration)\n            )\n        }\n\n        sqlite3_finalize(stmt)\n        return list\n    }\n\n    \/\/ \u66f4\u65b0\u64ad\u653e\u8fdb\u5ea6\n    func updateProgress(id: Int64, progress: Double) {\n        let sql = &quot;UPDATE video SET progress=? WHERE id=?&quot;\n        var stmt: OpaquePointer?\n        sqlite3_prepare_v2(db, sql, -1, &amp;amp;stmt, nil)\n        sqlite3_bind_double(stmt, 1, progress)\n        sqlite3_bind_int64(stmt, 2, id)\n        sqlite3_step(stmt)\n        sqlite3_finalize(stmt)\n    }\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">3\ufe0f\u20e3 <strong>VideoDownloader.swift \u2014 \u652f\u6301\u540e\u53f0\u4e0b\u8f7d\u89c6\u9891\u5230\u672c\u5730<\/strong><\/h1>\n\n\n\n<p>\u4e0b\u8f7d\u6210\u529f\u540e\u4f1a\u5199\u5165\u6570\u636e\u5e93\u3002<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport Foundation\n\nclass VideoDownloader: NSObject, URLSessionDownloadDelegate {\n    static let shared = VideoDownloader()\n\n    private lazy var session: URLSession = {\n        let config = URLSessionConfiguration.background(withIdentifier: &quot;video.download&quot;)\n        return URLSession(configuration: config, delegate: self, delegateQueue: nil)\n    }()\n\n    func downloadVideo(model: VideoModel) {\n        guard let urlStr = model.remoteURL,\n              let url = URL(string: urlStr) else { return }\n\n        let task = session.downloadTask(with: url)\n        task.taskDescription = model.title\n        task.resume()\n    }\n\n    \/\/ \u4e0b\u8f7d\u56de\u8c03\n    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,\n                    didFinishDownloadingTo location: URL) {\n\n        let fileName = (downloadTask.taskDescription ?? UUID().uuidString) + &quot;.mp4&quot;\n\n        let doc = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first!\n        let dest = URL(fileURLWithPath: doc).appendingPathComponent(fileName)\n\n        try? FileManager.default.moveItem(at: location, to: dest)\n\n        print(&quot;\ud83d\udce5 \u4e0b\u8f7d\u5b8c\u6210\uff1a\\(dest.path)&quot;)\n    }\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">4\ufe0f\u20e3 <strong>VideoPlayerManager.swift \u2014 \u6838\u5fc3\u64ad\u653e\u5668\u7ba1\u7406\u5668<\/strong><\/h1>\n\n\n\n<p>\u652f\u6301\uff1a\u5728\u7ebf\u64ad\u653e \/ \u672c\u5730\u64ad\u653e \/ \u81ea\u52a8\u9009\u62e9\u6a21\u5f0f<\/p>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nimport Foundation\nimport AVFoundation\nimport AVKit\n\nclass VideoPlayerManager {\n    static let shared = VideoPlayerManager()\n\n    private var player: AVPlayer?\n    private var timeObserver: Any?\n    private var currentID: Int64?\n\n    \/\/ \u64ad\u653e\u63a5\u53e3\uff08\u81ea\u52a8\u5224\u65ad\u672c\u5730 or \u5728\u7ebf\uff09\n    func play(_ model: VideoModel, on vc: UIViewController) {\n        \n        currentID = model.id\n\n        var url: URL?\n\n        if let local = model.filePath,\n           FileManager.default.fileExists(atPath: local) {\n            url = URL(fileURLWithPath: local)\n        } else if let remote = model.remoteURL {\n            url = URL(string: remote)\n        }\n\n        guard let videoURL = url else {\n            print(&quot;\u274c \u65e0\u53ef\u64ad\u653e\u8d44\u6e90&quot;)\n            return\n        }\n\n        let item = AVPlayerItem(url: videoURL)\n        player = AVPlayer(playerItem: item)\n\n        let pvc = AVPlayerViewController()\n        pvc.player = player\n        vc.present(pvc, animated: true) {\n            self.player?.play()\n        }\n\n        addProgressObserver()\n    }\n\n    \/\/ \u76d1\u542c\u8fdb\u5ea6\u5e76\u5199\u5165\u6570\u636e\u5e93\n    private func addProgressObserver() {\n        guard let player = player else { return }\n\n        timeObserver = player.addPeriodicTimeObserver(\n            forInterval: CMTime(seconds: 1, preferredTimescale: 1),\n            queue: .main\n        ) { &#x5B;weak self] time in\n            guard let self = self,\n                  let id = self.currentID\n            else { return }\n\n            let seconds = CMTimeGetSeconds(time)\n            VideoDatabase.shared.updateProgress(id: id, progress: seconds)\n        }\n    }\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">5\ufe0f\u20e3 <strong>\u4f7f\u7528\u793a\u4f8b\uff08ViewController.swift\uff09<\/strong><\/h1>\n\n\n<div class=\"wp-block-syntaxhighlighter-code \"><pre class=\"brush: plain; title: ; notranslate\" title=\"\">\nclass ViewController: UIViewController {\n    \n    override func viewDidLoad() {\n        super.viewDidLoad()\n\n        \/\/ \u793a\u4f8b\uff1a\u63d2\u5165\u4e00\u4e2a\u5728\u7ebf\u89c6\u9891\n        let m = VideoModel(\n            id: nil,\n            title: &quot;\u6f14\u793a\u89c6\u9891&quot;,\n            filePath: nil,\n            remoteURL: &quot;https:\/\/example.com\/demo.mp4&quot;,\n            progress: 0,\n            duration: 0\n        )\n        VideoDatabase.shared.insertVideo(m)\n\n        \/\/ \u67e5\u8be2\n        let list = VideoDatabase.shared.queryAll()\n        if let first = list.first {\n            VideoPlayerManager.shared.play(first, on: self)\n        }\n    }\n}\n\n<\/pre><\/div>\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83c\udf89 <strong>\u81f3\u6b64\uff0c\u4f60\u5df2\u62e5\u6709\u4e00\u4e2a\u5b8c\u6574\u201c\u5728\u7ebf\u89c6\u9891 + \u672c\u5730\u7f13\u5b58 + SQLite + \u64ad\u653e\u5668\u7ba1\u7406\u5668\u201d\u6a21\u5757\uff01<\/strong><\/h1>\n\n\n\n<p>\u5305\u542b\uff1a<\/p>\n\n\n\n<p>\u2714 \u5728\u7ebf\u64ad\u653e<br>\u2714 \u672c\u5730\u64ad\u653e<br>\u2714 \u81ea\u52a8\u9009\u62e9\u4f18\u5148\u672c\u5730<br>\u2714 \u89c6\u9891\u4e0b\u8f7d\u5230\u672c\u5730<br>\u2714 SQLite \u6301\u4e45\u5316<br>\u2714 \u64ad\u653e\u8fdb\u5ea6\u8bb0\u5f55<br>\u2714 AVPlayerViewController UI<br>\u2714 \u5b8c\u6574 Swift \u6a21\u5757\u5316\u6587\u4ef6<\/p>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>\u4e0b\u9762\u7ed9\u4f60\u4e00\u4efd\u8d85\u6e05\u6670\u3001\u53ef\u76f4\u63a5\u4e0a\u624b\u7684 AVPlayer \u64ad\u653e\u201c\u5728\u7ebf\u89c6\u9891 + \u672c\u5730\u6587\u4ef6&#8230; <a class=\"more-link\" href=\"https:\/\/www.52runoob.com\/index.php\/2025\/11\/29\/avplayer-%e6%92%ad%e6%94%be%e5%9c%a8%e7%ba%bf%e8%a7%86%e9%a2%91%e5%92%8c%e6%9c%ac%e5%9c%b0%e6%96%87%e4%bb%b6%e6%95%b0%e6%8d%ae%e5%ba%93%e8%ae%be%e7%bd%ae\/\">Continue Reading &rarr;<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[12],"tags":[],"class_list":["post-260","post","type-post","status-publish","format-standard","hentry","category-12"],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/260","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/comments?post=260"}],"version-history":[{"count":1,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/260\/revisions"}],"predecessor-version":[{"id":261,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/posts\/260\/revisions\/261"}],"wp:attachment":[{"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/media?parent=260"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/categories?post=260"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.52runoob.com\/index.php\/wp-json\/wp\/v2\/tags?post=260"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}