Ijkplayer音視頻開(kāi)發(fā)實(shí)戰(zhàn):實(shí)現(xiàn)高精度截圖與無(wú)損錄像功能
1. 項(xiàng)目緣起為什么要在Ijkplayer上做錄像和截圖做音視頻開(kāi)發(fā)的朋友對(duì)Ijkplayer這個(gè)名字應(yīng)該不陌生。作為一款基于FFmpeg的輕量級(jí)Android/iOS播放器內(nèi)核它憑借優(yōu)秀的兼容性和可定制性在眾多需要深度定制播放器的項(xiàng)目中扮演著核心角色。然而Ijkplayer官方庫(kù)更像是一個(gè)“純凈”的播放引擎它專(zhuān)注于解碼、渲染和播放控制對(duì)于很多業(yè)務(wù)場(chǎng)景中常見(jiàn)的“附加功能”比如錄像錄制正在播放的視頻流和截圖抓取當(dāng)前播放畫(huà)面并沒(méi)有提供開(kāi)箱即用的API。這就引出了一個(gè)非常實(shí)際的需求當(dāng)你的App基于Ijkplayer構(gòu)建了一個(gè)功能完善的播放器產(chǎn)品經(jīng)理突然提出“用戶(hù)需要能保存當(dāng)前播放的精彩瞬間”或者“希望能把這段直播錄下來(lái)回看”你該怎么辦你不可能去魔改FFmpeg的核心解碼流程也不可能讓用戶(hù)去系統(tǒng)相冊(cè)里翻找。這個(gè)需求必須在我們自己的應(yīng)用層或者說(shuō)在Ijkplayer的“外圍”來(lái)解決。我最近就在一個(gè)在線(xiàn)教育項(xiàng)目中遇到了這個(gè)需求。我們需要允許用戶(hù)對(duì)課程視頻進(jìn)行任意時(shí)刻的截圖保存并且對(duì)直播課的內(nèi)容進(jìn)行本地錄制方便課后復(fù)習(xí)。經(jīng)過(guò)一番折騰和踩坑最終形成了一套相對(duì)穩(wěn)定、高效的實(shí)現(xiàn)方案。今天我就把這套方案的核心思路、關(guān)鍵代碼以及那些官方文檔里不會(huì)寫(xiě)的“坑”和技巧毫無(wú)保留地分享出來(lái)。無(wú)論你是剛接觸Ijkplayer還是正在為類(lèi)似功能頭疼相信這篇內(nèi)容都能給你提供一條清晰的路徑。2. 核心思路拆解錄像與截圖的本質(zhì)差異在動(dòng)手寫(xiě)代碼之前我們必須從原理上厘清“錄像”和“截圖”這兩個(gè)功能在Ijkplayer上下文下的本質(zhì)區(qū)別。這決定了我們后續(xù)技術(shù)選型和實(shí)現(xiàn)路徑的完全不同。2.1 截圖單幀畫(huà)面的捕獲與編碼截圖本質(zhì)上是在某個(gè)精確的時(shí)間點(diǎn)獲取視頻渲染器SurfaceView/TextureView上當(dāng)前呈現(xiàn)的那一幀圖像數(shù)據(jù)并將其編碼成一張圖片通常是JPEG或PNG保存到本地。這里的關(guān)鍵在于“獲取數(shù)據(jù)源”。對(duì)于Ijkplayer我們通常有幾種思路從渲染層抓取這是最直觀(guān)的想法。既然畫(huà)面最終渲染到了SurfaceView或TextureView上那直接對(duì)這個(gè)View進(jìn)行截圖不就行了對(duì)于TextureView確實(shí)可以通過(guò)getBitmap()方法輕松拿到Bitmap對(duì)象。這個(gè)方案簡(jiǎn)單粗暴但存在一個(gè)致命問(wèn)題它截取的是經(jīng)過(guò)縮放、旋轉(zhuǎn)、添加了UI覆蓋層如播放按鈕、字幕之后的“最終屏幕畫(huà)面”。如果你需要的是純凈的、原始比例的視頻幀這個(gè)方案就不合適。從解碼層攔截這是更專(zhuān)業(yè)的做法。Ijkplayer在解碼視頻流后會(huì)得到原始的YUV或RGB幀數(shù)據(jù)然后才交給渲染器。我們可以在數(shù)據(jù)送往渲染器之前攔截下某一幀。這種方式能獲得最原始的、未經(jīng)過(guò)任何UI污染的圖像數(shù)據(jù)畫(huà)質(zhì)有保證并且可以獲取到精確到幀的時(shí)間戳信息。Ijkplayer的IjkMediaPlayer類(lèi)提供了一些擴(kuò)展接口允許我們注冊(cè)一個(gè)回調(diào)來(lái)獲取解碼后的視頻幀AVFrame這為我們實(shí)現(xiàn)方案二提供了可能。結(jié)論對(duì)于追求原始畫(huà)質(zhì)和精確控制的場(chǎng)景如專(zhuān)業(yè)工具、內(nèi)容審核從解碼層攔截幀數(shù)據(jù)是更優(yōu)選擇。而對(duì)于快速實(shí)現(xiàn)、且不介意包含播放器UI的普通場(chǎng)景從TextureView截圖也是一種可行的備選方案。2.2 錄像流媒體的重封裝與錄制錄像則復(fù)雜得多。它不再是獲取單張圖片而是要將正在播放的音視頻流在播放的同時(shí)另存為一個(gè)新的媒體文件如MP4。這個(gè)過(guò)程可以類(lèi)比為“搭橋”播放器從網(wǎng)絡(luò)或本地讀取原始流A點(diǎn)解碼后送到渲染器和揚(yáng)聲器B點(diǎn)。我們需要在A點(diǎn)到B點(diǎn)之間的某個(gè)位置分出一條支流C點(diǎn)將原始的、或者解碼后再編碼的音視頻數(shù)據(jù)按照時(shí)間順序重新封裝成一個(gè)文件。這里的技術(shù)路線(xiàn)選擇直接決定了實(shí)現(xiàn)的復(fù)雜度、性能開(kāi)銷(xiāo)和最終文件的質(zhì)量方案A錄屏Screen Recording這是操作系統(tǒng)級(jí)別的功能。在Android上你可以使用MediaProjectionAPI來(lái)錄制整個(gè)屏幕或指定窗口的內(nèi)容。這個(gè)方案完全獨(dú)立于Ijkplayer你錄下的是包括播放器界面、系統(tǒng)狀態(tài)欄在內(nèi)的所有東西。它的優(yōu)點(diǎn)是實(shí)現(xiàn)相對(duì)標(biāo)準(zhǔn)不依賴(lài)播放器內(nèi)部邏輯缺點(diǎn)是無(wú)法分離純音視頻流文件體積大可能涉及隱私權(quán)限需要用戶(hù)授權(quán)并且在不同系統(tǒng)版本上兼容性不一。方案B傳輸流錄制Stream Recording這是更貼近“錄像”本質(zhì)的方案。我們錄制的是Ijkplayer接收到的原始數(shù)據(jù)流。如果播放的是MP4文件或HTTP-FLV/TS流我們可以在網(wǎng)絡(luò)層或解復(fù)用demux之后直接將接收到的音視頻包如H.264 NALU, AAC幀寫(xiě)入一個(gè)新的文件容器中。這個(gè)過(guò)程不需要重新編碼因此性能損耗極低畫(huà)質(zhì)無(wú)損文件體積也相對(duì)較小。但它的局限性也很明顯只能錄制播放器當(dāng)前支持的封裝格式并且如果流本身是加密的或特殊編碼就無(wú)法直接保存。方案C解碼后重新編碼錄制Transcoding Recording這是最通用但也是最重的方案。我們像截圖方案二那樣從解碼后拿到原始的音頻幀PCM和視頻幀YUV/RGB然后利用如MediaCodec、FFmpeg或MediaRecorder等編碼器將它們重新編碼如H.264AAC再封裝成MP4。這個(gè)方案靈活性最高可以統(tǒng)一輸出格式、調(diào)整分辨率、碼率但會(huì)帶來(lái)巨大的CPU/GPU開(kāi)銷(xiāo)對(duì)設(shè)備性能要求高不適合長(zhǎng)時(shí)間錄制。結(jié)論對(duì)于大多數(shù)播放器內(nèi)錄像需求方案B傳輸流錄制是平衡性能、畫(huà)質(zhì)和實(shí)現(xiàn)復(fù)雜度的最佳選擇前提是片源格式支持。方案C可以作為格式轉(zhuǎn)換或處理的備選但需謹(jǐn)慎評(píng)估性能。方案A錄屏更適用于錄制整個(gè)App操作流程而非單純的播放內(nèi)容?;谝陨戏治霰疚膶⒅攸c(diǎn)分享截圖采用從解碼層攔截AVFrame的方案實(shí)現(xiàn)高畫(huà)質(zhì)、精確的截圖。錄像采用傳輸流錄制的方案實(shí)現(xiàn)高性能、無(wú)損的直播/視頻錄制。3. 環(huán)境準(zhǔn)備與Ijkplayer深度集成在開(kāi)始編碼前我們需要搭建好開(kāi)發(fā)環(huán)境并對(duì)Ijkplayer有更深入的了解特別是如何訪(fǎng)問(wèn)其內(nèi)部數(shù)據(jù)。3.1 Ijkplayer的引入與配置首先通過(guò)Gradle引入Ijkplayer。推薦使用維護(hù)較新的分支或自己編譯以獲得更多可控的接口。// 在項(xiàng)目的build.gradle中添加jitpack倉(cāng)庫(kù)如果使用jitpack版本 allprojects { repositories { ... maven { url https://jitpack.io } } } // 在app模塊的build.gradle中添加依賴(lài) dependencies { implementation com.github.CarGuo.GSYVideoPlayer:gsyVideoPlayer-java:v8.3.5 // 這是一個(gè)包含ijkplayer的流行播放器庫(kù)也可直接用純ijkplayer // 或者使用純ijkplayer // implementation tv.danmaku.ijk.media:ijkplayer-java:0.8.8 // implementation tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8 // 根據(jù)需要添加其他架構(gòu) }如果你需要自定義編譯開(kāi)啟特定的編解碼器或協(xié)議支持比如對(duì)于錄像功能確保mpegts、hls等demuxer已開(kāi)啟則需要下載Ijkplayer源碼參考其官方文檔進(jìn)行編譯。這個(gè)過(guò)程較為復(fù)雜但能獲得最大的靈活性。3.2 理解Ijkplayer的數(shù)據(jù)管道與擴(kuò)展點(diǎn)Ijkplayer的核心是IjkMediaPlayer它封裝了FFmpeg的AVFormatContext、AVCodecContext等核心結(jié)構(gòu)。要實(shí)現(xiàn)我們的高級(jí)功能必須與其內(nèi)部狀態(tài)進(jìn)行交互。監(jiān)聽(tīng)器與回調(diào)標(biāo)準(zhǔn)的MediaPlayer監(jiān)聽(tīng)器OnInfoListener,OnErrorListener用于處理播放狀態(tài)、分辨率變化等通用事件。IjkMediaPlayer的擴(kuò)展選項(xiàng)IjkMediaPlayer提供了setOption方法可以配置大量FFmpeg級(jí)別的參數(shù)這對(duì)調(diào)試和功能啟用至關(guān)重要。Native層回調(diào)這是我們實(shí)現(xiàn)截圖功能的關(guān)鍵。Ijkplayer在Native層C代碼提供了鉤子函數(shù)hook允許我們?cè)谝曨l幀解碼后、渲染前拿到AVFrame數(shù)據(jù)。Java層需要通過(guò)JNI接口來(lái)設(shè)置這個(gè)回調(diào)。通常一個(gè)深度集成的Ijkplayer播放器類(lèi)會(huì)這樣初始化public class CustomIjkPlayer { private IjkMediaPlayer mMediaPlayer; public void initPlayer() { mMediaPlayer new IjkMediaPlayer(); // 設(shè)置一些常用選項(xiàng)提升兼容性 mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec, 1L); // 開(kāi)啟硬解 mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, mediacodec-auto-rotate, 1L); mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_PLAYER, opensles, 0L); mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_FORMAT, http-detect-range-support, 0L); mMediaPlayer.setOption(IjkMediaPlayer.OPT_CATEGORY_CODEC, skip_loop_filter, 48L); // 跳幀策略 // 設(shè)置監(jiān)聽(tīng)器 mMediaPlayer.setOnPreparedListener(...); mMediaPlayer.setOnInfoListener(...); mMediaPlayer.setOnErrorListener(...); // ... 其他監(jiān)聽(tīng)器 } }準(zhǔn)備工作就緒后我們就可以進(jìn)入具體的功能實(shí)現(xiàn)了。4. 實(shí)戰(zhàn)實(shí)現(xiàn)高精度視頻截圖功能我們選擇從解碼層攔截AVFrame的方案。這需要修改或擴(kuò)展Ijkplayer的Native代碼并建立JNI橋接。4.1 建立Native層幀捕獲回調(diào)這是整個(gè)截圖功能最核心也最復(fù)雜的一步。你需要修改Ijkplayer的C源碼通常是ijkplayer/ijkmedia/ijkplayer/目錄下的文件。步驟一定義回調(diào)接口在某個(gè)頭文件如frame_capture.h中定義幀數(shù)據(jù)回調(diào)的函數(shù)指針類(lèi)型。// frame_capture.h #ifndef IJKFRAME_CAPTURE_H #define IJKFRAME_CAPTURE_H #include libavutil/frame.h typedef void (*on_frame_captured)(AVFrame *frame, void *opaque); // 設(shè)置全局回調(diào)函數(shù) void set_frame_capture_callback(on_frame_captured callback, void *opaque); #endif步驟二在視頻渲染線(xiàn)程注入回調(diào)找到視頻幀解碼后、準(zhǔn)備渲染前的函數(shù)例如在ffplay.c的video_refresh函數(shù)內(nèi)部或在ijksdl_vout.c的渲染函數(shù)中。在合適的時(shí)機(jī)調(diào)用我們?cè)O(shè)置的回調(diào)函數(shù)。// 在某個(gè)視頻處理函數(shù)中比如處理完一個(gè)AVFrame后 static void video_image_display2(...) { AVFrame *frame ...; // 獲取到當(dāng)前要渲染的幀 // ... 其他處理邏輯 // 調(diào)用截圖回調(diào) extern on_frame_captured g_frame_callback; extern void *g_callback_opaque; if (g_frame_callback frame) { // 注意這里傳遞的是AVFrame的指針確保在回調(diào)使用期間frame有效。 // 一種穩(wěn)妥的做法是av_frame_ref一個(gè)副本傳給回調(diào)由回調(diào)方負(fù)責(zé)釋放。 AVFrame *frame_copy av_frame_alloc(); av_frame_ref(frame_copy, frame); g_frame_callback(frame_copy, g_callback_opaque); } // ... 后續(xù)渲染邏輯 }步驟三實(shí)現(xiàn)JNI橋接創(chuàng)建JNI文件如ijkplayer_android.c暴露一個(gè)Java可調(diào)用的Native方法用于設(shè)置回調(diào)。同時(shí)在JNI層實(shí)現(xiàn)一個(gè)函數(shù)當(dāng)Native回調(diào)觸發(fā)時(shí)通過(guò)JNI將幀數(shù)據(jù)如轉(zhuǎn)換為RGB格式的字節(jié)數(shù)組或直接傳遞YUV數(shù)據(jù)回調(diào)到Java層。// ijkplayer_android.c Java_com_yourpackage_CustomIjkPlayer_setFrameCaptureCallback(JNIEnv *env, jobject thiz) { set_frame_capture_callback(my_native_frame_callback, (void*)env-NewGlobalRef(thiz)); } // Native層的回調(diào)函數(shù) void my_native_frame_callback(AVFrame *frame, void *opaque) { JNIEnv *env ...; // 獲取JNIEnv注意線(xiàn)程綁定 jobject java_obj (jobject)opaque; // 將AVFrame數(shù)據(jù)轉(zhuǎn)換為Java可用的格式例如將YUV轉(zhuǎn)換為RGB Bitmap所需的字節(jié)數(shù)組 // 這是一個(gè)復(fù)雜的過(guò)程涉及色彩空間轉(zhuǎn)換和內(nèi)存拷貝 // 1. 獲取frame的寬高、格式 // 2. 使用sws_scale將frame數(shù)據(jù)轉(zhuǎn)換到目標(biāo)格式如AV_PIX_FMT_RGBA // 3. 將轉(zhuǎn)換后的數(shù)據(jù)通過(guò)JNI傳遞給Java // 偽代碼示例 // jbyteArray data env-NewByteArray(rgb_size); // env-SetByteArrayRegion(data, 0, rgb_size, (jbyte*)rgb_buffer); // env-CallVoidMethod(java_obj, jmethod_onFrameCaptured, data, width, height); // env-DeleteLocalRef(data); av_frame_free(frame); // 釋放副本 }4.2 Java層接收與處理幀數(shù)據(jù)在Java層我們需要定義一個(gè)接口來(lái)接收Native層回調(diào)過(guò)來(lái)的幀數(shù)據(jù)。public interface FrameCaptureListener { /** * 當(dāng)捕獲到一幀視頻數(shù)據(jù)時(shí)回調(diào) * param data RGB或RGBA格式的字節(jié)數(shù)組 * param width 幀寬度 * param height 幀高度 * param format 像素格式例如 Bitmap.Config.ARGB_8888 */ void onFrameCaptured(byte[] data, int width, int height, Bitmap.Config format); }在自定義的播放器類(lèi)中public class CustomIjkPlayer { private FrameCaptureListener mCaptureListener; private IjkMediaPlayer mMediaPlayer; // 設(shè)置監(jiān)聽(tīng)器 public void setFrameCaptureListener(FrameCaptureListener listener) { this.mCaptureListener listener; if (listener ! null) { // 調(diào)用Native方法啟用幀捕獲 nativeSetFrameCaptureEnabled(true); } else { nativeSetFrameCaptureEnabled(false); } } // 供JNI調(diào)用的方法 CalledByNative private void onFrameData(byte[] rgbData, int width, int height) { if (mCaptureListener ! null) { runOnUiThread(() - { // 注意在UI線(xiàn)程創(chuàng)建Bitmap Bitmap bitmap Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); ByteBuffer buffer ByteBuffer.wrap(rgbData); bitmap.copyPixelsFromBuffer(buffer); mCaptureListener.onFrameCaptured(bitmap); // 可以傳遞Bitmap更方便 }); } } // 外部調(diào)用的截圖方法 public void captureCurrentFrame() { // 這個(gè)方法并不是立刻就能拿到圖而是觸發(fā)一個(gè)信號(hào) // 讓Native層在下一幀渲染時(shí)回調(diào)onFrameData。 // 可以通過(guò)設(shè)置一個(gè)標(biāo)志位在my_native_frame_callback中檢查該標(biāo)志位為真時(shí)才回調(diào)Java層。 nativeRequestFrameCapture(); } // Native方法聲明 private native void nativeSetFrameCaptureEnabled(boolean enabled); private native void nativeRequestFrameCapture(); // ... 加載native庫(kù) static { System.loadLibrary(your-ijkplayer-lib); } }4.3 將Bitmap保存至相冊(cè)拿到Bitmap對(duì)象后保存到相冊(cè)就是標(biāo)準(zhǔn)的Android操作了。注意Android QAPI 29及以上版本作用域存儲(chǔ)Scoped Storage的權(quán)限變化。public class BitmapSaver { public static void saveBitmapToGallery(Context context, Bitmap bitmap, String fileName, OnSaveListener listener) { if (bitmap null || bitmap.isRecycled()) { listener.onFailed(Bitmap is invalid); return; } // 1. 創(chuàng)建圖片文件MediaStore方式兼容Android Q ContentValues values new ContentValues(); values.put(MediaStore.Images.Media.DISPLAY_NAME, fileName); values.put(MediaStore.Images.Media.MIME_TYPE, image/jpeg); values.put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES /YourAppName); ContentResolver resolver context.getContentResolver(); Uri uri null; try { uri resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values); if (uri null) { listener.onFailed(Failed to create new MediaStore record.); return; } // 2. 將Bitmap寫(xiě)入OutputStream OutputStream os resolver.openOutputStream(uri); if (os null) { listener.onFailed(Failed to open output stream.); return; } boolean success bitmap.compress(Bitmap.CompressFormat.JPEG, 90, os); // JPEG質(zhì)量90% os.close(); if (success) { // 3. 通知系統(tǒng)相冊(cè)更新可選但建議 Intent mediaScanIntent new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); mediaScanIntent.setData(uri); context.sendBroadcast(mediaScanIntent); listener.onSuccess(uri); } else { resolver.delete(uri, null, null); // 刪除失敗的記錄 listener.onFailed(Failed to compress bitmap.); } } catch (IOException e) { e.printStackTrace(); if (uri ! null) { resolver.delete(uri, null, null); } listener.onFailed(IO Error: e.getMessage()); } } public interface OnSaveListener { void onSuccess(Uri savedUri); void onFailed(String errorMsg); } }關(guān)鍵注意事項(xiàng)與踩坑點(diǎn)性能與內(nèi)存AVFrame到Bitmap的轉(zhuǎn)換特別是sws_scale是CPU密集型操作頻繁截圖會(huì)導(dǎo)致卡頓。務(wù)必在非UI線(xiàn)程執(zhí)行并考慮降低截圖分辨率在Native層轉(zhuǎn)換時(shí)指定小一點(diǎn)的寬高。線(xiàn)程安全Native回調(diào)可能發(fā)生在非UI線(xiàn)程操作UI或創(chuàng)建Bitmap必須切回主線(xiàn)程。幀時(shí)效性captureCurrentFrame()是異步的它請(qǐng)求的是“下一幀”或“最近一幀”而不是“當(dāng)前屏幕顯示的那一幀”。對(duì)于精確到秒的截圖需求這個(gè)延遲通??梢越邮堋H绻蠼^對(duì)精確可能需要結(jié)合播放器的當(dāng)前時(shí)間戳和幀回調(diào)時(shí)間戳做更復(fù)雜的同步。格式兼容確保Native層轉(zhuǎn)換的像素格式如AV_PIX_FMT_RGBA與Java層Bitmap.Config如ARGB_8888匹配否則顏色會(huì)錯(cuò)亂。5. 實(shí)戰(zhàn)實(shí)現(xiàn)高效流媒體錄制功能我們采用傳輸流錄制方案B。核心思想是復(fù)制Ijkplayer讀取到的原始數(shù)據(jù)包并將其寫(xiě)入一個(gè)新的文件。5.1 攔截與復(fù)制數(shù)據(jù)包同樣我們需要深入到Ijkplayer的數(shù)據(jù)讀取層。Ijkplayer通過(guò)AVFormatContext的read_frame函數(shù)讀取音視頻包AVPacket。我們需要在這個(gè)讀取動(dòng)作發(fā)生后將AVPacket復(fù)制一份。步驟一修改Read Thread找到Ijkplayer中負(fù)責(zé)讀包的線(xiàn)程通常在ffplay.c的read_thread函數(shù)里。在av_read_frame調(diào)用成功后將讀取到的AVPacket分發(fā)出去。// 在read_thread中 for (;;) { AVPacket pkt; ret av_read_frame(ic, pkt); if (ret 0) { break; // 讀取結(jié)束或出錯(cuò) } // ... 原有的隊(duì)列推送邏輯 // 新增錄制邏輯 if (is_recording_enabled) { // 復(fù)制AVPacket。注意av_packet_ref會(huì)增加buf的引用計(jì)數(shù)管理好內(nèi)存。 AVPacket pkt_copy; av_init_packet(pkt_copy); if (av_packet_ref(pkt_copy, pkt) 0) { // 將pkt_copy放入一個(gè)錄制專(zhuān)用的隊(duì)列由另一個(gè)錄制線(xiàn)程消費(fèi) packet_recorder_push(pkt_copy); } } // 結(jié)束新增 av_packet_unref(pkt); // 釋放原始pkt }步驟二創(chuàng)建錄制線(xiàn)程與隊(duì)列我們需要一個(gè)單獨(dú)的線(xiàn)程和線(xiàn)程安全的隊(duì)列來(lái)處理錄制任務(wù)避免阻塞主播放線(xiàn)程。// 定義一個(gè)線(xiàn)程安全的AVPacket隊(duì)列 typedef struct PacketRecorder { AVFifoBuffer *pkt_fifo; pthread_mutex_t mutex; pthread_cond_t cond; int abort_request; // ... 其他狀態(tài)如輸出格式上下文等 } PacketRecorder; // 初始化、銷(xiāo)毀、推送、彈出隊(duì)列的函數(shù) PacketRecorder* recorder_init(); void recorder_destroy(PacketRecorder* r); int recorder_push_packet(PacketRecorder* r, AVPacket *pkt); int recorder_pop_packet(PacketRecorder* r, AVPacket *pkt, int block);錄制線(xiàn)程的主循環(huán)大致如下static void *record_thread(void *arg) { PacketRecorder *recorder (PacketRecorder *)arg; AVPacket pkt; // 1. 創(chuàng)建輸出格式上下文AVFormatContext AVFormatContext *oc NULL; avformat_alloc_output_context2(oc, NULL, mp4, NULL); // 輸出為mp4 // ... 配置輸出流stream需要從輸入流復(fù)制codecpar信息 // 2. 寫(xiě)文件頭 avformat_write_header(oc, NULL); while (!recorder-abort_request) { if (recorder_pop_packet(recorder, pkt, 1) 0) { continue; } // 3. 重新計(jì)算時(shí)間戳關(guān)鍵 // 輸入流的時(shí)間戳基準(zhǔn)time_base和輸出流可能不同需要轉(zhuǎn)換。 av_packet_rescale_ts(pkt, input_stream-time_base, output_stream-time_base); // 4. 寫(xiě)入文件 av_interleaved_write_frame(oc, pkt); av_packet_unref(pkt); } // 5. 寫(xiě)文件尾 av_write_trailer(oc); // 6. 清理資源 avformat_free_context(oc); return NULL; }5.2 Java層控制錄制生命周期在Java層我們需要提供開(kāi)始、停止錄制的接口并管理Native層錄制線(xiàn)程的狀態(tài)。public class CustomIjkPlayer { // ... 其他成員 public boolean startRecording(String outputPath) { if (mMediaPlayer null || isRecording) { return false; } // 調(diào)用Native方法傳入輸出文件路徑啟動(dòng)錄制線(xiàn)程 boolean success nativeStartRecording(outputPath); if (success) { isRecording true; } return success; } public void stopRecording() { if (isRecording) { nativeStopRecording(); isRecording false; // 可以在這里通知錄制完成處理文件等 } } private native boolean nativeStartRecording(String outputPath); private native void nativeStopRecording(); }5.3 處理時(shí)間戳與文件封裝這是錄像功能中最容易出錯(cuò)的部分。時(shí)間戳轉(zhuǎn)換原始流中的AVPacket可能有dts解碼時(shí)間戳和pts顯示時(shí)間戳。在錄制時(shí)必須使用av_packet_rescale_ts函數(shù)將它們從輸入流的time_base轉(zhuǎn)換到輸出流的time_base。如果轉(zhuǎn)換錯(cuò)誤會(huì)導(dǎo)致錄制的視頻播放速度異常、音畫(huà)不同步。流復(fù)制創(chuàng)建輸出流時(shí)需要使用avcodec_parameters_copy將輸入流的AVCodecParameters復(fù)制到輸出流。這確保了編碼信息如編碼格式、分辨率、采樣率的一致性。切記不要?jiǎng)?chuàng)建新的編碼器我們是在做“流復(fù)制”不是“轉(zhuǎn)碼”。文件格式avformat_alloc_output_context2的第三個(gè)參數(shù)指定封裝格式如mp4,flv,mpegts。你需要根據(jù)輸入流的格式和你的需求來(lái)選擇。MP4是通用性最好的選擇之一。寫(xiě)入模式使用av_interleaved_write_frame而不是av_write_frame前者會(huì)幫你處理音視頻包的交錯(cuò)interleaving確保生成的文件更規(guī)范。關(guān)鍵注意事項(xiàng)與踩坑點(diǎn)內(nèi)存管理AVPacket和AVFrame一樣需要嚴(yán)格配對(duì)使用av_packet_ref/av_packet_unref或av_packet_alloc/av_packet_free否則會(huì)造成內(nèi)存泄漏。錄制隊(duì)列要及時(shí)消費(fèi)避免堆積過(guò)多數(shù)據(jù)包導(dǎo)致內(nèi)存暴漲。線(xiàn)程同步播放線(xiàn)程和錄制線(xiàn)程通過(guò)隊(duì)列通信必須做好加鎖pthread_mutex_t和條件變量pthread_cond_t同步防止競(jìng)態(tài)條件。錄制時(shí)機(jī)最好在播放器prepared之后、開(kāi)始播放之前啟動(dòng)錄制以確保能錄到完整的文件頭信息。對(duì)于直播流則隨時(shí)可以開(kāi)始。文件寫(xiě)入確保應(yīng)用有外部存儲(chǔ)的寫(xiě)入權(quán)限WRITE_EXTERNAL_STORAGEAndroid Q以上需要使用MediaStore API或應(yīng)用專(zhuān)屬目錄。性能影響雖然流復(fù)制開(kāi)銷(xiāo)遠(yuǎn)小于轉(zhuǎn)碼但頻繁的IO操作和內(nèi)存拷貝仍會(huì)對(duì)性能有影響在低端設(shè)備上長(zhǎng)時(shí)間錄制需關(guān)注發(fā)熱和耗電。6. 進(jìn)階優(yōu)化與問(wèn)題排查基礎(chǔ)功能實(shí)現(xiàn)后我們還需要考慮穩(wěn)定性、兼容性和用戶(hù)體驗(yàn)。6.1 截圖功能的優(yōu)化策略異步與隊(duì)列在Java層設(shè)立一個(gè)截圖任務(wù)隊(duì)列。當(dāng)用戶(hù)快速連續(xù)點(diǎn)擊截圖時(shí)將請(qǐng)求入隊(duì)由單獨(dú)的線(xiàn)程按順序處理Native回調(diào)、格式轉(zhuǎn)換和保存避免阻塞UI或造成幀丟失。分辨率可調(diào)在Native層回調(diào)中可以傳遞目標(biāo)寬高給sws_scale直接生成縮略圖尺寸的Bitmap節(jié)省內(nèi)存和存儲(chǔ)空間。格式選擇提供JPEG有損、體積小和PNG無(wú)損、體積大的選項(xiàng)。在Bitmap.compress方法中切換CompressFormat即可。精確時(shí)間戳在Native回調(diào)時(shí)除了幀數(shù)據(jù)一并傳遞該幀的pts顯示時(shí)間戳。在Java層可以將這個(gè)時(shí)間戳作為文件名的一部分實(shí)現(xiàn)按時(shí)間點(diǎn)精準(zhǔn)命名。6.2 錄像功能的健壯性處理異常處理錄制過(guò)程中可能發(fā)生磁盤(pán)寫(xiě)滿(mǎn)、網(wǎng)絡(luò)中斷對(duì)于網(wǎng)絡(luò)流等情況。Native層需要將錯(cuò)誤碼通過(guò)JNI回傳給Java層Java層再通知用戶(hù)。錄制狀態(tài)同步確保startRecording和stopRecording的調(diào)用是冪等的。避免重復(fù)開(kāi)始或重復(fù)停止。文件完整性即使錄制被意外中斷如App崩潰也應(yīng)盡量調(diào)用av_write_trailer來(lái)寫(xiě)入文件尾部分播放器可能能播放不完整的MP4。更好的做法是定期將數(shù)據(jù)flush到磁盤(pán)。格式兼容性檢查不是所有輸入流都適合直接復(fù)刻為MP4。在開(kāi)始錄制前可以檢查輸入流的編碼格式codec_id和封裝格式如果不支持則回退到方案C轉(zhuǎn)碼錄制或提示用戶(hù)不支持。6.3 常見(jiàn)問(wèn)題排查指南截圖黑屏或花屏檢查Native回調(diào)是否被觸發(fā)在my_native_frame_callback中添加日志確認(rèn)有數(shù)據(jù)過(guò)來(lái)。檢查像素格式轉(zhuǎn)換確認(rèn)sws_scale轉(zhuǎn)換的源格式和目標(biāo)格式正確。最常見(jiàn)的錯(cuò)誤是YUV的平面格式如YUV420P轉(zhuǎn)換錯(cuò)誤。檢查Bitmap創(chuàng)建確認(rèn)Java層收到的width和height大于0并且Bitmap.createBitmap成功。檢查OpenGL渲染如果使用了OpenGL渲染android_opengl渲染器幀數(shù)據(jù)可能不在通常的CPU內(nèi)存路徑上需要從GPU內(nèi)存讀取這需要不同的處理方式如使用glReadPixels。錄制的視頻無(wú)法播放或異常檢查時(shí)間戳這是首要懷疑對(duì)象。用ffprobe工具查看原視頻和錄制視頻的包時(shí)間戳對(duì)比是否規(guī)律。av_packet_rescale_ts的參數(shù)是否正確檢查流參數(shù)復(fù)制確認(rèn)avcodec_parameters_copy成功執(zhí)行輸出流的codecpar信息完整。檢查文件頭尾確保avformat_write_header和av_write_trailer都被成功調(diào)用??梢杂胒fmpeg -i your_record.mp4檢查文件信息是否完整。檢查隊(duì)列阻塞如果錄制線(xiàn)程消費(fèi)太慢可能導(dǎo)致包隊(duì)列堆積內(nèi)存溢出。添加隊(duì)列長(zhǎng)度監(jiān)控和丟棄策略。性能問(wèn)題CPU占用過(guò)高排查是截圖轉(zhuǎn)換sws_scale還是錄像寫(xiě)文件導(dǎo)致的。使用性能分析工具如Android Profiler定位熱點(diǎn)。內(nèi)存泄漏嚴(yán)格檢查所有AVPacket和AVFrame的引用計(jì)數(shù)管理。確保每個(gè)av_packet_ref都有對(duì)應(yīng)的av_packet_unref每個(gè)av_frame_alloc都有對(duì)應(yīng)的av_frame_free。7. 替代方案與第三方庫(kù)考量如果你覺(jué)得修改Ijkplayer源碼過(guò)于復(fù)雜或者項(xiàng)目時(shí)間緊迫也有一些替代路徑可以考慮使用TextureView截圖如前所述如果對(duì)畫(huà)質(zhì)要求不高TextureView.getBitmap()是最快的實(shí)現(xiàn)方式。你只需要在播放器類(lèi)中暴露這個(gè)View然后在合適的時(shí)機(jī)如點(diǎn)擊截圖按鈕時(shí)調(diào)用即可。但要注意線(xiàn)程安全和Bitmap內(nèi)存回收。使用更高層次的播放器庫(kù)一些基于Ijkplayer封裝的、功能更全面的播放器庫(kù)如GSYVideoPlayer、JiaoZiVideoPlayer等它們有時(shí)會(huì)內(nèi)置截圖或錄制功能或者提供了更易擴(kuò)展的接口。你可以研究其源碼看是否能滿(mǎn)足需求或在其基礎(chǔ)上修改。獨(dú)立的錄屏方案對(duì)于錄像如果內(nèi)容允許使用Android系統(tǒng)的MediaProjectionAPI是一個(gè)完全解耦的方案。你需要處理權(quán)限、管理MediaRecorder或ImageReader并處理音軌的混合系統(tǒng)錄屏可能不包含播放器內(nèi)部音頻。這個(gè)方案更通用但定制性差且界面無(wú)法隱藏。FFmpeg命令行封裝一個(gè)“野路子”但有時(shí)很有效的錄像方案將播放的網(wǎng)絡(luò)流URL直接傳遞給一個(gè)在后臺(tái)運(yùn)行的FFmpeg命令行進(jìn)程讓它去拉流并保存到文件。這完全 bypass 了播放器但需要處理FFmpeg可執(zhí)行文件的打包、進(jìn)程管理以及資源消耗。每種方案都有其權(quán)衡。修改Ijkplayer源碼提供了最大的靈活性和最佳的性能/效果但代價(jià)是復(fù)雜度高、維護(hù)成本高。你需要根據(jù)項(xiàng)目的具體需求、團(tuán)隊(duì)的技術(shù)能力和時(shí)間預(yù)算來(lái)做出選擇。在我經(jīng)歷的項(xiàng)目中對(duì)于需要深度集成、且對(duì)畫(huà)質(zhì)和性能有要求的場(chǎng)景直接修改Ijkplayer源碼是繞不開(kāi)的路。這個(gè)過(guò)程雖然繁瑣但一旦走通你對(duì)多媒體播放的理解會(huì)深刻很多后續(xù)應(yīng)對(duì)其他定制化需求也會(huì)更加得心應(yīng)手。上面的代碼和思路已經(jīng)勾勒出了主要的框架你可以將其作為起點(diǎn)填充細(xì)節(jié)調(diào)試適配最終構(gòu)建出穩(wěn)定可靠的播放器增強(qiáng)功能。

相關(guān)新聞

樹(shù)莓派雙通道CAN FD HAT開(kāi)發(fā)指南:從硬件選型到Python實(shí)戰(zhàn)

樹(shù)莓派雙通道CAN FD HAT開(kāi)發(fā)指南:從硬件選型到Python實(shí)戰(zhàn)

1. 項(xiàng)目概述:2-CH CAN FD HAT是什么?如果你手頭有樹(shù)莓派,又想讓它跟汽車(chē)、工業(yè)設(shè)備或者機(jī)器人“聊聊天”,那這個(gè)2-CH CAN FD HAT可能就是你要找的“翻譯官”。簡(jiǎn)單來(lái)說(shuō),它是一塊可以插在樹(shù)莓派GPIO接口上的擴(kuò)展板&…

2026/8/1 13:00:42 閱讀更多
Python調(diào)用FFmpeg報(bào)錯(cuò)OSError: [Errno 2]的全面診斷與解決方案

Python調(diào)用FFmpeg報(bào)錯(cuò)OSError: [Errno 2]的全面診斷與解決方案

1. 問(wèn)題概述:當(dāng)Python遇上FFmpeg的“幽靈文件”如果你在用Python腳本調(diào)用FFmpeg進(jìn)行推流、轉(zhuǎn)碼或任何音視頻處理時(shí),突然蹦出來(lái)一個(gè)OSError: [Errno 2] No such file or directory,那一刻的心情,恐怕和深夜加班時(shí)發(fā)現(xiàn)咖啡機(jī)壞了差不…

2026/8/1 13:00:42 閱讀更多
STC89C52驅(qū)動(dòng)DS18B20:?jiǎn)慰偩€(xiàn)協(xié)議深度解析與穩(wěn)定測(cè)溫實(shí)戰(zhàn)

STC89C52驅(qū)動(dòng)DS18B20:?jiǎn)慰偩€(xiàn)協(xié)議深度解析與穩(wěn)定測(cè)溫實(shí)戰(zhàn)

1. 項(xiàng)目緣起:為什么還在用STC89C52和DS18B20?最近在整理一些老項(xiàng)目的資料,翻出來(lái)一個(gè)基于STC89C52和DS18B20的溫度監(jiān)測(cè)小模塊??赡苡腥藭?huì)問(wèn),現(xiàn)在STM32、ESP32滿(mǎn)天飛,性能強(qiáng)、外設(shè)多、開(kāi)發(fā)方便,為什么還要去…

2026/8/1 12:50:41 閱讀更多
【鎖3】Semaphore(信號(hào)量)

【鎖3】Semaphore(信號(hào)量)

Semaphore(信號(hào)量)的核心是用 N 張“許可證”限制同時(shí)訪(fǎng)問(wèn)資源的線(xiàn)程數(shù)——線(xiàn)程進(jìn)來(lái)先 acquire() 拿一張,用完 release() 還回去;許可證發(fā)完了,后面的線(xiàn)程就得排隊(duì)。下面給你 Java 最常用版本的可運(yùn)行示例。 核心概念 …

2026/8/1 14:01:08 閱讀更多
MiniMax H3 深度解析:打破模態(tài)界限,全能多模態(tài)模型優(yōu)勢(shì)在哪

MiniMax H3 深度解析:打破模態(tài)界限,全能多模態(tài)模型優(yōu)勢(shì)在哪

今天,正式發(fā)布 MiniMax H3。這是一款通用的全模態(tài)生成模型,支持對(duì)文本、圖像、視頻、聲音組成的多模態(tài)上下文的統(tǒng)一理解能力、能夠輸出具備原生雙聲道的音視頻,最高可支持 15s 2K 分辨率。 MiniMax H3 具備商用級(jí)的多場(chǎng)景內(nèi)容生成能力&#x…

2026/8/1 14:01:08 閱讀更多
STM32 FSMC驅(qū)動(dòng)SSD1963 LCD:8080時(shí)序硬件解析與實(shí)戰(zhàn)

STM32 FSMC驅(qū)動(dòng)SSD1963 LCD:8080時(shí)序硬件解析與實(shí)戰(zhàn)

1. 項(xiàng)目概述:從時(shí)序圖到點(diǎn)亮屏幕的完整旅程 搞嵌入式顯示的朋友,對(duì)SSD1963這顆經(jīng)典的LCD驅(qū)動(dòng)芯片應(yīng)該不陌生。它支持高達(dá)864x480的分辨率、24位真彩色,還能直接掛載SDRAM作為顯存,在當(dāng)年算是相當(dāng)強(qiáng)悍的“顯卡”了。但它的8080并行…

2026/8/1 14:01:08 閱讀更多
2026最新!糾正孩子發(fā)音的3款實(shí)用英語(yǔ)聽(tīng)說(shuō)軟件

2026最新!糾正孩子發(fā)音的3款實(shí)用英語(yǔ)聽(tīng)說(shuō)軟件

【摘要:】 本文結(jié)合我5年英語(yǔ)聽(tīng)說(shuō)教學(xué)和工具測(cè)評(píng)經(jīng)驗(yàn),拆解當(dāng)前少兒英語(yǔ)發(fā)音糾正的核心痛點(diǎn),對(duì)比3款主流工具的技術(shù)邏輯和實(shí)測(cè)效果,給出中立選型參考,所有數(shù)據(jù)均來(lái)自實(shí)測(cè)和公開(kāi)技術(shù)白皮書(shū),無(wú)軟廣。 我當(dāng)初20…

2026/8/1 14:01:08 閱讀更多
解決Chrome無(wú)法訪(fǎng)問(wèn)localhost:6667:不安全端口ERR_UNSAFE_PORT的完整指南

解決Chrome無(wú)法訪(fǎng)問(wèn)localhost:6667:不安全端口ERR_UNSAFE_PORT的完整指南

1. 問(wèn)題現(xiàn)象與核心原因剖析當(dāng)你興致勃勃地在本地啟動(dòng)了一個(gè)開(kāi)發(fā)服務(wù),比如一個(gè)Spring Boot應(yīng)用或者一個(gè)Node.js的API后端,配置的端口是6667,然后在谷歌瀏覽器(Chrome)里滿(mǎn)懷期待地輸入http://localhost:6667/your/api/p…

2026/8/1 13:50:45 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應(yīng)用材料(Applied Materials)公司生產(chǎn)的一款用于半導(dǎo)體設(shè)備的I/O信號(hào)分配電路板。該型號(hào)(0100-02186)的核心特點(diǎn)如下:專(zhuān)用于Endura等半導(dǎo)體工藝腔室。集成信號(hào)路由與分配功能。連接控制…

2026/8/1 0:09:33 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機(jī),適用于自動(dòng)化設(shè)備及通用機(jī)械驅(qū)動(dòng)。該型號(hào)(FFMN-32L-10-T0 40AX)的核心特點(diǎn)如下:三相交流異步電動(dòng)機(jī)。額定…

2026/8/1 0:09:33 閱讀更多
AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O 分配 PCB

AMAT 0100-02186 I/O分配PCB板是應(yīng)用材料(Applied Materials)公司生產(chǎn)的一款用于半導(dǎo)體設(shè)備的I/O信號(hào)分配電路板。該型號(hào)(0100-02186)的核心特點(diǎn)如下:專(zhuān)用于Endura等半導(dǎo)體工藝腔室。集成信號(hào)路由與分配功能。連接控制…

2026/8/1 0:09:33 閱讀更多
Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)

Nissei Corp FFMN-32L-10-T0 40AX 三相異步電動(dòng)機(jī)是日本日清(Nissei)品牌的一款工業(yè)用三相異步電機(jī),適用于自動(dòng)化設(shè)備及通用機(jī)械驅(qū)動(dòng)。該型號(hào)(FFMN-32L-10-T0 40AX)的核心特點(diǎn)如下:三相交流異步電動(dòng)機(jī)。額定…

2026/8/1 0:09:33 閱讀更多