前言
最近在做一個PC端小應用,需要獲取攝像頭畫面,但是電腦攝像頭像素太低,而且位置調整不方便,又不想為此單獨買個攝像頭,于是想起了之前淘汰掉的手機,成像質量還是杠杠的,能不能把手機攝像頭連接到電腦上使用呢?經過搜索,在網上找到了幾款這類應用,但是都是閉源的,我一向偏好使用開源軟體,但是找了挺久也沒有找到一個比較合適的,想著算了,自己開發一個吧,反正這么個簡單的需求,應該大概也許不難吧(??
思路
通過Android的Camera API是可以拿到攝像頭每一幀的原始影像資料的,一般都是YUV格式的資料,一幀2400x1080的圖片大小為2400x1080x3/2位元組,約等于3.7M,25fps的話,帶寬要達到741mbps,太費帶寬了,所以只能壓縮一下再傳輸了,最簡單的方法,把每一幀壓縮成jpeg再傳輸,就是效率有點低,而更好的方法是壓縮成視頻流后再傳輸,PC端接收到視頻流后再實時解壓碩訓原回圖片,
實作
思路有了,那就開搞吧,
獲取攝像頭資料
新建一個Android專案,然后在AndroidManifest.xml
中宣告攝像頭和網路權限:
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.INTERNET" />
界面上搞一個SurfaceView
用于預覽
<SurfaceView
android:id="@+id/surfaceview"
android:layout_
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并呼叫攝像頭預覽介面開始獲取攝像頭資料:
camera.setPreviewDisplay(surfaceHolder);
buffer.data = https://www.cnblogs.com/kason/archive/2023/07/07/new byte[bufferSize];
camera.setPreviewCallbackWithBuffer(this);
camera.addCallbackBuffer(buffer.data);
camera.startPreview();
每一幀影像的資料準備好后,會通過onPreviewFrame回呼把YUV資料傳送過來,處理完后,一定要再調一次addCallbackBuffer
以獲取下一幀的資料,
@Override
public void onPreviewFrame(byte[] data, Camera c) {
// data就是原始YUV資料
// 這里處理YUV資料
camera.addCallbackBuffer(buffer.data);
}
監聽PC端連接
直接用ServerSocket就行了,反正也不需要考慮高并發場景,
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上可以使用系統自帶的MediaCodec
實作視頻編解碼,但是這里我并不打算使用它,而是使用靈活度更高的ffmpeg(誰知道后面有沒有一些奇奇怪怪的需求??????), 網上已經有大神封裝好適用于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
類可以實作視頻錄制功能,但是靈活度太低,還是直接調原生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;
}
把攝像頭資料送進來編碼,由于攝像頭獲取到的資料格式和視頻編碼需要的資料格式往往不一樣,所以,編碼前需要呼叫sws_scale
對影像資料進行格式轉換,
public int recordFrame(Frame frame) {
byte[] data = https://www.cnblogs.com/kason/archive/2023/07/07/frame.data; // 對應onPreviewFrame回呼里的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) {
// 影像資料格式轉換
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 = https://www.cnblogs.com/kason/archive/2023/07/07/pkg.data();
if (pkgData == null) {
return;
}
pkgData.get(outBuffer, 0, pkg.size());
os.write(outBuffer, 0, pkg.size());
重點流程的代碼都寫好了,把它們連接起來就可以收工了,
收尾
請求端還沒寫好,先在電腦端使用ffplay測驗一下,
ffplay tcp://手機IP:6666
嗯,一切正常!就是延時有點大,主要是ffplay不知道視頻流的格式,所以緩沖了很多幀的資料來偵測視頻格式,造成了較大的延時,后面有時間,再寫篇使用ffmpeg api實時解碼H264的文章(??
完整專案代碼:https://github.com/kasonyang/net-camera
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/556819.html
標籤:其他
上一篇:flutter小白是如何在一周內用chatGPT開發一款App的
下一篇:返回列表