Android實時獲取攝像頭畫面?zhèn)鬏斨罰C端思路詳解
前言
最近在做一個PC端小應用,需要獲取攝像頭畫面,但是電腦攝像頭像素太低,而且位置調(diào)整不方便,又不想為此單獨買個攝像頭。于是想起了之前淘汰掉的手機,成像質(zhì)量還是杠杠的,能不能把手機攝像頭連接到電腦上使用呢?經(jīng)過搜索,在網(wǎng)上找到了幾款這類應用,但是都是閉源的。我一向偏好使用開源軟件,但是找了挺久也沒有找到一個比較合適的。想著算了,自己開發(fā)一個吧,反正這么個簡單的需求,應該大概也許不難吧(??
思路
通過Android的Camera API是可以拿到攝像頭每一幀的原始圖像數(shù)據(jù)的,一般都是YUV格式的數(shù)據(jù),一幀2400x1080的圖片大小為2400x1080x3/2字節(jié),約等于3.7M。25fps的話,帶寬要達到741mbps,太費帶寬了,所以只能壓縮一下再傳輸了。最簡單的方法,把每一幀壓縮成jpeg再傳輸,就是效率有點低,而更好的方法是壓縮成視頻流后再傳輸,PC端接收到視頻流后再實時解壓縮還原回圖片。
實現(xiàn)
思路有了,那就開搞吧。
獲取攝像頭數(shù)據(jù)
新建一個Android項目,然后在AndroidManifest.xml
中聲明攝像頭和網(wǎng)絡權限:
<uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.INTERNET" />
界面上搞一個SurfaceView
用于預覽
<SurfaceView android:id="@+id/surfaceview" android:layout_width="fill_parent" android:layout_height="fill_parent" />
進入主Activity時,打開攝像頭:
private void openCamera(int cameraId) { class CameraHandlerThread extends HandlerThread { private Handler mHandler; public CameraHandlerThread(String name) { super(name); start(); mHandler = new Handler(getLooper()); } synchronized void notifyCameraOpened() { notify(); } void openCamera() { mHandler.post(() -> { camera = Camera.open(cameraId); notifyCameraOpened(); }); try { wait(); } catch (InterruptedException e) { Log.w(TAG, "wait was interrupted"); } } } if (camera == null) { CameraHandlerThread mThread = new CameraHandlerThread("camera thread"); synchronized (mThread) { mThread.openCamera(); } } }
然后綁定預覽surface并調(diào)用攝像頭預覽接口開始獲取攝像頭數(shù)據(jù):
camera.setPreviewDisplay(surfaceHolder); buffer.data = new byte[bufferSize]; camera.setPreviewCallbackWithBuffer(this); camera.addCallbackBuffer(buffer.data); camera.startPreview();
每一幀圖像的數(shù)據(jù)準備好后,會通過onPreviewFrame回調(diào)把YUV數(shù)據(jù)傳送過來,處理完后,一定要再調(diào)一次addCallbackBuffer
以獲取下一幀的數(shù)據(jù)。
@Override public void onPreviewFrame(byte[] data, Camera c) { // data就是原始YUV數(shù)據(jù) // 這里處理YUV數(shù)據(jù) camera.addCallbackBuffer(buffer.data); }
監(jiān)聽PC端連接
直接用ServerSocket就行了,反正也不需要考慮高并發(fā)場景。
try (ServerSocket srvSocket = new ServerSocket(6666)) { this.socketServer = srvSocket; for (; ; ) { Socket socket = srvSocket.accept(); this.outputStream = new DataOutputStream(socket.getOutputStream()); // 初始化視頻編碼器 } } catch (IOException ex) { Log.e(TAG, ex.getMessage(), ex); }
視頻編碼
Android上可以使用系統(tǒng)自帶的MediaCodec
實現(xiàn)視頻編解碼,但是這里我并不打算使用它,而是使用靈活度更高的ffmpeg(誰知道后面有沒有一些奇奇怪怪的需求??????)。 網(wǎng)上已經(jīng)有大神封裝好適用于Android的ffmpeg了,直接在Gradle上引用javacv
庫就行。
configurations { javacpp } task javacppExtract(type: Copy) { dependsOn configurations.javacpp from { configurations.javacpp.collect { zipTree(it) } } include "lib/**" into "$buildDir/javacpp/" android.sourceSets.main.jniLibs.srcDirs += ["$buildDir/javacpp/lib/"] tasks.getByName('preBuild').dependsOn javacppExtract } dependencies { implementation group: 'org.bytedeco', name: 'javacv', version: '1.5.9' javacpp group: 'org.bytedeco', name: 'openblas-platform', version: '0.3.23-1.5.9' javacpp group: 'org.bytedeco', name: 'opencv-platform', version: '4.7.0-1.5.9' javacpp group: 'org.bytedeco', name: 'ffmpeg-platform', version: '6.0-1.5.9' }
javacv
庫自帶了一個FFmpegFrameRecorder
類可以實現(xiàn)視頻錄制功能,但是靈活度太低,還是直接調(diào)原生ffmpeg接口吧。
初始化H264編碼器:
public void init(int width, int height, int[] preferredPixFmt) throws IOException { int bitRate = width * height * 3 / 2 * 16; int frameRate = 25; encoder = avcodec_find_encoder(AV_CODEC_ID_H264); codecCtx = initCodecCtx(width, height, fmt, bitRate, frameRate); tempFrame = av_frame_alloc(); scaledFrame = av_frame_alloc(); tempFrame.pts(-1); packet = av_packet_alloc(); } private AVCodecContext initCodecCtx(int width, int height,int pixFmt, int bitRate, int frameRate) { AVCodecContext codec_ctx = avcodec_alloc_context3(encoder); codec_ctx.codec_id(AV_CODEC_ID_H264); codec_ctx.pix_fmt(pixFmt); codec_ctx.width(width); codec_ctx.height(height); codec_ctx.bit_rate(bitRate); codec_ctx.rc_buffer_size(bitRate); codec_ctx.framerate().num(frameRate); codec_ctx.framerate().den(1); codec_ctx.gop_size(frameRate);//每秒1個關鍵幀 codec_ctx.time_base().num(1); codec_ctx.time_base().den(frameRate); codec_ctx.has_b_frames(0); codec_ctx.global_quality(1); codec_ctx.max_b_frames(0); av_opt_set(codec_ctx.priv_data(), "tune", "zerolatency", 0); av_opt_set(codec_ctx.priv_data(), "preset", "ultrafast", 0); int ret = avcodec_open2(codec_ctx, encoder, (AVDictionary) null); return ret == 0 ? codec_ctx : null; }
把攝像頭數(shù)據(jù)送進來編碼,由于攝像頭獲取到的數(shù)據(jù)格式和視頻編碼需要的數(shù)據(jù)格式往往不一樣,所以,編碼前需要調(diào)用sws_scale
對圖像數(shù)據(jù)進行格式轉(zhuǎn)換。
public int recordFrame(Frame frame) { byte[] data = frame.data; // 對應onPreviewFrame回調(diào)里的data int pf = frame.pixelFormat; if (tempFrameDataLen < data.length) { if (tempFrameData != null) { tempFrameData.releaseReference(); } tempFrameData = new BytePointer(data.length); tempFrameDataLen = data.length; } tempFrameData.put(data); int width = frame.width; int height = frame.height; av_image_fill_arrays(tempFrame.data(), tempFrame.linesize(), tempFrameData, pf, width, height, frame.align); tempFrame.format(pf); tempFrame.width(width); tempFrame.height(height); tempFrame.pts(tempFrame.pts() + 1); return recordFrame(tempFrame); } public int recordFrame(AVFrame frame) { int res = 0; int srcFmt = frame.format(); int dstFmt = codecCtx.pix_fmt(); int width = frame.width(); int height = frame.height(); if (srcFmt != dstFmt) { // 圖像數(shù)據(jù)格式轉(zhuǎn)換 convertCtx = sws_getCachedContext( convertCtx, width, height, srcFmt, width, height, dstFmt, SWS_BILINEAR, null, null, (DoublePointer) null ); int requiredDataLen = width * height * 3 / 2; if (scaledFrameDataLen < requiredDataLen) { if (scaledFrameData != null) { scaledFrameData.releaseReference(); } scaledFrameData = new BytePointer(requiredDataLen); scaledFrameDataLen = requiredDataLen; } av_image_fill_arrays(scaledFrame.data(), scaledFrame.linesize(), scaledFrameData, dstFmt, width, height, 1); scaledFrame.format(dstFmt); scaledFrame.width(width); scaledFrame.height(height); scaledFrame.pts(frame.pts()); res = sws_scale(convertCtx, frame.data(), frame.linesize(), 0, height, scaledFrame.data(), scaledFrame.linesize()); if (res == 0) { throw new RuntimeException("scale frame failed"); } frame = scaledFrame; } res = avcodec_send_frame(codecCtx, frame); scaledFrame.pts(scaledFrame.pts() + 1); if (res != 0 && res != AVERROR_EAGAIN()) { throw new RuntimeException("Failed to encode frame:" + res); } res = avcodec_receive_packet(codecCtx, packet); if (res != 0 && res != AVERROR_EAGAIN()) { return res; } return res; }
編碼完一幀圖像后,需要檢查是否有AVPacket
生成,如果有,把它回寫給請求端即可。
AVPacket pkg = encoder.getPacket(); if (outBuffer == null || outBuffer.length < pkg.size()) { outBuffer = new byte[pkg.size()]; } BytePointer pkgData = pkg.data(); if (pkgData == null) { return; } pkgData.get(outBuffer, 0, pkg.size()); os.write(outBuffer, 0, pkg.size());
重點流程的代碼都寫好了,把它們連接起來就可以收工了。
收尾
請求端還沒寫好,先在電腦端使用ffplay測試一下。
ffplay tcp://手機IP:6666
嗯,一切正常!就是延時有點大,主要是ffplay不知道視頻流的格式,所以緩沖了很多幀的數(shù)據(jù)來偵測視頻格式,造成了較大的延時。后面有時間,再寫篇使用ffmpeg api實時解碼H264的文章(??
完整項目代碼:https://github.com/kasonyang/net-camera
到此這篇關于Android實時獲取攝像頭畫面?zhèn)鬏斨罰C端的文章就介紹到這了,更多相關Android獲取攝像頭畫面內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
Android BroadcastReceiver常見監(jiān)聽整理
這篇文章主要介紹了Android BroadcastReceiver常見監(jiān)聽整理的相關資料,需要的朋友可以參考下2016-10-10Android使用SQLite數(shù)據(jù)庫的示例
本篇文章主要介紹了Android使用SQLite數(shù)據(jù)庫的示例,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2017-01-01Android Fragment實現(xiàn)列表和內(nèi)容聯(lián)動
這篇文章主要為大家詳細介紹了Android Fragment實現(xiàn)列表和內(nèi)容聯(lián)動效果,具有一定的參考價值,感興趣的小伙伴們可以參考一下2019-01-01修改Android Studio 的 Logcat 緩沖區(qū)大小操作
這篇文章主要介紹了修改Android Studio 的 Logcat 緩沖區(qū)大小操作,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧2020-04-04Android之Intent附加數(shù)據(jù)的兩種實現(xiàn)方法
這篇文章主要介紹了Android之Intent附加數(shù)據(jù)的兩種實現(xiàn)方法,以實例形式較為詳細的分析了添加數(shù)據(jù)到Intent的相關技巧,具有一定參考借鑒價值,需要的朋友可以參考下2015-09-09Android 組件Gallery和GridView示例講解
本文主要講解Android 組件Gallery和GridView,這里詳細介紹組件Gallery和GridView的知識要點,并附示例代碼和實現(xiàn)效果圖,有興趣的小伙伴可以參考下2016-08-08