jiangdongguo
7 years ago
21 changed files with 3353 additions and 1697 deletions
@ -1,198 +0,0 @@ |
|||||
/* |
|
||||
* UVCCamera |
|
||||
* library and sample to access to UVC web camera on non-rooted Android device |
|
||||
* |
|
||||
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
|
||||
* |
|
||||
* Licensed under the Apache License, Version 2.0 (the "License"); |
|
||||
* you may not use this file except in compliance with the License. |
|
||||
* You may obtain a copy of the License at |
|
||||
* |
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||
* |
|
||||
* Unless required by applicable law or agreed to in writing, software |
|
||||
* distributed under the License is distributed on an "AS IS" BASIS, |
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
|
||||
* See the License for the specific language governing permissions and |
|
||||
* limitations under the License. |
|
||||
* |
|
||||
* All files in the folder are under this Apache License, Version 2.0. |
|
||||
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
|
||||
* may have a different license, see the respective files. |
|
||||
*/ |
|
||||
|
|
||||
package com.jiangdg.usbcamera.view; |
|
||||
|
|
||||
import android.Manifest; |
|
||||
import android.annotation.SuppressLint; |
|
||||
import android.app.Activity; |
|
||||
import android.content.pm.PackageManager; |
|
||||
import android.os.Bundle; |
|
||||
import android.os.Handler; |
|
||||
import android.os.Looper; |
|
||||
import android.support.annotation.NonNull; |
|
||||
import android.support.annotation.StringRes; |
|
||||
import android.support.v7.app.AppCompatActivity; |
|
||||
import android.util.Log; |
|
||||
import android.widget.Toast; |
|
||||
|
|
||||
import com.serenegiant.dialog.MessageDialogFragment; |
|
||||
import com.serenegiant.utils.BuildCheck; |
|
||||
import com.serenegiant.utils.HandlerThreadHandler; |
|
||||
import com.serenegiant.utils.PermissionCheck; |
|
||||
|
|
||||
/** |
|
||||
* Created by saki on 2016/11/18. |
|
||||
* |
|
||||
*/ |
|
||||
public class BaseActivity extends AppCompatActivity { |
|
||||
|
|
||||
private static boolean DEBUG = false; |
|
||||
private static final String TAG = BaseActivity.class.getSimpleName(); |
|
||||
|
|
||||
// 处理UI的Handler
|
|
||||
private final Handler mUIHandler = new Handler(Looper.getMainLooper()); |
|
||||
private final Thread mUiThread = mUIHandler.getLooper().getThread(); |
|
||||
// 工作线程Handler
|
|
||||
private Handler mWorkerHandler; |
|
||||
private long mWorkerThreadID = -1; |
|
||||
|
|
||||
@Override |
|
||||
protected void onCreate(final Bundle savedInstanceState) { |
|
||||
super.onCreate(savedInstanceState); |
|
||||
// 创建工作线程
|
|
||||
if (mWorkerHandler == null) { |
|
||||
mWorkerHandler = HandlerThreadHandler.createHandler(TAG); |
|
||||
mWorkerThreadID = mWorkerHandler.getLooper().getThread().getId(); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected void onPause() { |
|
||||
clearToast(); |
|
||||
super.onPause(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected synchronized void onDestroy() { |
|
||||
// 释放线程资源
|
|
||||
if (mWorkerHandler != null) { |
|
||||
try { |
|
||||
mWorkerHandler.getLooper().quit(); |
|
||||
} catch (final Exception e) { |
|
||||
//
|
|
||||
} |
|
||||
mWorkerHandler = null; |
|
||||
} |
|
||||
super.onDestroy(); |
|
||||
} |
|
||||
|
|
||||
//================================================================================
|
|
||||
/** |
|
||||
* |
|
||||
* 子线程中更新UI,duration为延迟多久执行 |
|
||||
* |
|
||||
*/ |
|
||||
public final void runOnUiThread(final Runnable task, final long duration) { |
|
||||
if (task == null) return; |
|
||||
mUIHandler.removeCallbacks(task); |
|
||||
if ((duration > 0) || Thread.currentThread() != mUiThread) { |
|
||||
mUIHandler.postDelayed(task, duration); |
|
||||
} else { |
|
||||
try { |
|
||||
task.run(); |
|
||||
} catch (final Exception e) { |
|
||||
Log.w(TAG, e); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 移除更新UI task |
|
||||
* @param task |
|
||||
*/ |
|
||||
public final void removeFromUiThread(final Runnable task) { |
|
||||
if (task == null) return; |
|
||||
mUIHandler.removeCallbacks(task); |
|
||||
} |
|
||||
|
|
||||
/** |
|
||||
* 工作子线程中执行的任务 |
|
||||
*/ |
|
||||
protected final synchronized void queueEvent(final Runnable task, final long delayMillis) { |
|
||||
if ((task == null) || (mWorkerHandler == null)) return; |
|
||||
try { |
|
||||
mWorkerHandler.removeCallbacks(task); |
|
||||
if (delayMillis > 0) { |
|
||||
mWorkerHandler.postDelayed(task, delayMillis); |
|
||||
} else if (mWorkerThreadID == Thread.currentThread().getId()) { |
|
||||
task.run(); |
|
||||
} else { |
|
||||
mWorkerHandler.post(task); |
|
||||
} |
|
||||
} catch (final Exception e) { |
|
||||
// ignore
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
protected final synchronized void removeEvent(final Runnable task) { |
|
||||
if (task == null) return; |
|
||||
try { |
|
||||
mWorkerHandler.removeCallbacks(task); |
|
||||
} catch (final Exception e) { |
|
||||
// ignore
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
//================================================================================
|
|
||||
private Toast mToast; |
|
||||
|
|
||||
protected void showToast(@StringRes final int msg, final Object... args) { |
|
||||
removeFromUiThread(mShowToastTask); |
|
||||
mShowToastTask = new ShowToastTask(msg, args); |
|
||||
runOnUiThread(mShowToastTask, 0); |
|
||||
} |
|
||||
|
|
||||
|
|
||||
protected void clearToast() { |
|
||||
removeFromUiThread(mShowToastTask); |
|
||||
mShowToastTask = null; |
|
||||
try { |
|
||||
if (mToast != null) { |
|
||||
mToast.cancel(); |
|
||||
mToast = null; |
|
||||
} |
|
||||
} catch (final Exception e) { |
|
||||
// ignore
|
|
||||
} |
|
||||
} |
|
||||
|
|
||||
private ShowToastTask mShowToastTask; |
|
||||
private final class ShowToastTask implements Runnable { |
|
||||
final int msg; |
|
||||
final Object args; |
|
||||
private ShowToastTask(@StringRes final int msg, final Object... args) { |
|
||||
this.msg = msg; |
|
||||
this.args = args; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void run() { |
|
||||
try { |
|
||||
if (mToast != null) { |
|
||||
mToast.cancel(); |
|
||||
mToast = null; |
|
||||
} |
|
||||
if (args != null) { |
|
||||
final String _msg = getString(msg, args); |
|
||||
mToast = Toast.makeText(BaseActivity.this, _msg, Toast.LENGTH_SHORT); |
|
||||
} else { |
|
||||
mToast = Toast.makeText(BaseActivity.this, msg, Toast.LENGTH_SHORT); |
|
||||
} |
|
||||
mToast.show(); |
|
||||
} catch (final Exception e) { |
|
||||
// ignore
|
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -1,250 +0,0 @@ |
|||||
package com.jiangdg.usbcamera.view; |
|
||||
|
|
||||
import android.hardware.usb.UsbDevice; |
|
||||
import android.os.Bundle; |
|
||||
import android.util.Log; |
|
||||
import android.view.Surface; |
|
||||
import android.view.SurfaceHolder; |
|
||||
import android.view.SurfaceView; |
|
||||
import android.view.View; |
|
||||
import android.view.View.OnClickListener; |
|
||||
import android.widget.ImageButton; |
|
||||
import android.widget.Toast; |
|
||||
|
|
||||
import com.jiangdg.usbcamera.R; |
|
||||
import com.serenegiant.usb.CameraDialog; |
|
||||
import com.serenegiant.usb.USBMonitor; |
|
||||
import com.serenegiant.usb.USBMonitor.OnDeviceConnectListener; |
|
||||
import com.serenegiant.usb.USBMonitor.UsbControlBlock; |
|
||||
import com.serenegiant.usb.UVCCamera; |
|
||||
|
|
||||
import butterknife.BindView; |
|
||||
import butterknife.ButterKnife; |
|
||||
import butterknife.OnClick; |
|
||||
|
|
||||
public class MainActivity extends BaseActivity implements CameraDialog.CameraDialogParent { |
|
||||
private static final boolean DEBUG = true; |
|
||||
private static final String TAG = "MainActivity"; |
|
||||
@BindView(R.id.camera_surface_view) |
|
||||
public SurfaceView mUVCCameraView; |
|
||||
|
|
||||
private final Object mSync = new Object(); |
|
||||
// USB和USB Camera访问管理类
|
|
||||
private USBMonitor mUSBMonitor; |
|
||||
private UVCCamera mUVCCamera; |
|
||||
|
|
||||
private Surface mPreviewSurface; |
|
||||
private boolean isActive, isPreview; |
|
||||
|
|
||||
private final SurfaceHolder.Callback mSurfaceViewCallback = new SurfaceHolder.Callback() { |
|
||||
@Override |
|
||||
public void surfaceCreated(final SurfaceHolder holder) { |
|
||||
if (DEBUG) Log.v(TAG, "surfaceCreated:"); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void surfaceChanged(final SurfaceHolder holder, final int format, final int width, final int height) { |
|
||||
if ((width == 0) || (height == 0)) return; |
|
||||
if (DEBUG) Log.v(TAG, "surfaceChanged:"); |
|
||||
mPreviewSurface = holder.getSurface(); |
|
||||
synchronized (mSync) { |
|
||||
if (isActive && !isPreview && (mUVCCamera != null)) { |
|
||||
mUVCCamera.setPreviewDisplay(mPreviewSurface); |
|
||||
mUVCCamera.startPreview(); |
|
||||
isPreview = true; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void surfaceDestroyed(final SurfaceHolder holder) { |
|
||||
if (DEBUG) Log.v(TAG, "surfaceDestroyed:"); |
|
||||
synchronized (mSync) { |
|
||||
if (mUVCCamera != null) { |
|
||||
mUVCCamera.stopPreview(); |
|
||||
} |
|
||||
isPreview = false; |
|
||||
} |
|
||||
mPreviewSurface = null; |
|
||||
} |
|
||||
}; |
|
||||
|
|
||||
@Override |
|
||||
protected void onCreate(final Bundle savedInstanceState) { |
|
||||
super.onCreate(savedInstanceState); |
|
||||
setContentView(R.layout.activity_main); |
|
||||
// 绑定Activity
|
|
||||
ButterKnife.bind(this); |
|
||||
mUVCCameraView.getHolder().addCallback(mSurfaceViewCallback); |
|
||||
|
|
||||
// 初始化USBMonitor
|
|
||||
// 注册USB设备监听器
|
|
||||
mUSBMonitor = new USBMonitor(this, new OnDeviceConnectListener() { |
|
||||
@Override |
|
||||
public void onAttach(final UsbDevice device) { |
|
||||
if (DEBUG) Log.v(TAG, "onAttach:"); |
|
||||
Toast.makeText(MainActivity.this, "检测到USB设备", Toast.LENGTH_SHORT).show(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onConnect(final UsbDevice device, final UsbControlBlock ctrlBlock, final boolean createNew) { |
|
||||
if (DEBUG) Log.v(TAG, "onConnect:"); |
|
||||
Toast.makeText(MainActivity.this, "成功连接到USB设备", Toast.LENGTH_SHORT).show(); |
|
||||
synchronized (mSync) { |
|
||||
if (mUVCCamera != null) { |
|
||||
mUVCCamera.destroy(); |
|
||||
} |
|
||||
isActive = isPreview = false; |
|
||||
} |
|
||||
queueEvent(new Runnable() { |
|
||||
@Override |
|
||||
public void run() { |
|
||||
synchronized (mSync) { |
|
||||
final UVCCamera camera = new UVCCamera(); |
|
||||
camera.open(ctrlBlock); |
|
||||
if (DEBUG) Log.i(TAG, "supportedSize:" + camera.getSupportedSize()); |
|
||||
try { |
|
||||
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.FRAME_FORMAT_MJPEG); |
|
||||
} catch (final IllegalArgumentException e) { |
|
||||
try { |
|
||||
// fallback to YUV mode
|
|
||||
camera.setPreviewSize(UVCCamera.DEFAULT_PREVIEW_WIDTH, UVCCamera.DEFAULT_PREVIEW_HEIGHT, UVCCamera.DEFAULT_PREVIEW_MODE); |
|
||||
} catch (final IllegalArgumentException e1) { |
|
||||
camera.destroy(); |
|
||||
return; |
|
||||
} |
|
||||
} |
|
||||
mPreviewSurface = mUVCCameraView.getHolder().getSurface(); |
|
||||
if (mPreviewSurface != null) { |
|
||||
isActive = true; |
|
||||
camera.setPreviewDisplay(mPreviewSurface); |
|
||||
camera.startPreview(); |
|
||||
isPreview = true; |
|
||||
} |
|
||||
synchronized (mSync) { |
|
||||
mUVCCamera = camera; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, 0); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onDisconnect(final UsbDevice device, final UsbControlBlock ctrlBlock) { |
|
||||
if (DEBUG) Log.v(TAG, "onDisconnect:"); |
|
||||
Toast.makeText(MainActivity.this, "与USB设备断开连接", Toast.LENGTH_SHORT).show(); |
|
||||
// XXX you should check whether the comming device equal to camera device that currently using
|
|
||||
queueEvent(new Runnable() { |
|
||||
@Override |
|
||||
public void run() { |
|
||||
synchronized (mSync) { |
|
||||
if (mUVCCamera != null) { |
|
||||
mUVCCamera.close(); |
|
||||
if (mPreviewSurface != null) { |
|
||||
mPreviewSurface.release(); |
|
||||
mPreviewSurface = null; |
|
||||
} |
|
||||
isActive = isPreview = false; |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
}, 0); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onDettach(final UsbDevice device) { |
|
||||
if (DEBUG) Log.v(TAG, "onDettach:"); |
|
||||
Toast.makeText(MainActivity.this, "未检测到USB设备", Toast.LENGTH_SHORT).show(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onCancel(final UsbDevice device) { |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected void onStart() { |
|
||||
super.onStart(); |
|
||||
if (DEBUG) Log.v(TAG, "onStart:"); |
|
||||
synchronized (mSync) { |
|
||||
// 注册
|
|
||||
if (mUSBMonitor != null) { |
|
||||
mUSBMonitor.register(); |
|
||||
} |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected void onStop() { |
|
||||
if (DEBUG) Log.v(TAG, "onStop:"); |
|
||||
synchronized (mSync) { |
|
||||
// 注销
|
|
||||
if (mUSBMonitor != null) { |
|
||||
mUSBMonitor.unregister(); |
|
||||
} |
|
||||
} |
|
||||
super.onStop(); |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
protected void onDestroy() { |
|
||||
if (DEBUG) Log.v(TAG, "onDestroy:"); |
|
||||
synchronized (mSync) { |
|
||||
isActive = isPreview = false; |
|
||||
if (mUVCCamera != null) { |
|
||||
mUVCCamera.destroy(); |
|
||||
mUVCCamera = null; |
|
||||
} |
|
||||
// 释放资源
|
|
||||
if (mUSBMonitor != null) { |
|
||||
mUSBMonitor.destroy(); |
|
||||
mUSBMonitor = null; |
|
||||
} |
|
||||
} |
|
||||
mUVCCameraView = null; |
|
||||
super.onDestroy(); |
|
||||
} |
|
||||
|
|
||||
@OnClick({R.id.camera_surface_view}) |
|
||||
public void onViewClicked(View view){ |
|
||||
int vId= view.getId(); |
|
||||
switch (vId){ |
|
||||
case R.id.camera_surface_view: |
|
||||
if (mUVCCamera == null) { |
|
||||
// XXX calling CameraDialog.showDialog is necessary at only first time(only when app has no permission).
|
|
||||
// 当APP访问USB设备没有被授权时,弹出对话框
|
|
||||
CameraDialog.showDialog(MainActivity.this); |
|
||||
} else { |
|
||||
synchronized (mSync) { |
|
||||
mUVCCamera.destroy(); |
|
||||
mUVCCamera = null; |
|
||||
isActive = isPreview = false; |
|
||||
} |
|
||||
} |
|
||||
break; |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
|
|
||||
/** |
|
||||
* to access from CameraDialog |
|
||||
* @return |
|
||||
*/ |
|
||||
@Override |
|
||||
public USBMonitor getUSBMonitor() { |
|
||||
return mUSBMonitor; |
|
||||
} |
|
||||
|
|
||||
@Override |
|
||||
public void onDialogResult(boolean canceled) { |
|
||||
if (canceled) { |
|
||||
runOnUiThread(new Runnable() { |
|
||||
@Override |
|
||||
public void run() { |
|
||||
// FIXME
|
|
||||
} |
|
||||
}, 0); |
|
||||
} |
|
||||
} |
|
||||
} |
|
@ -0,0 +1,169 @@ |
|||||
|
package com.jiangdg.usbcamera.view; |
||||
|
|
||||
|
import android.hardware.usb.UsbDevice; |
||||
|
import android.os.Bundle; |
||||
|
import android.support.annotation.Nullable; |
||||
|
import android.support.v7.app.AppCompatActivity; |
||||
|
import android.view.View; |
||||
|
import android.widget.Button; |
||||
|
import android.widget.Toast; |
||||
|
|
||||
|
import com.jiangdg.usbcamera.R; |
||||
|
import com.jiangdg.usbcamera.USBCameraManager; |
||||
|
import com.serenegiant.usb.CameraDialog; |
||||
|
import com.serenegiant.usb.USBMonitor; |
||||
|
import com.serenegiant.usb.widget.CameraViewInterface; |
||||
|
|
||||
|
import butterknife.BindView; |
||||
|
import butterknife.ButterKnife; |
||||
|
import butterknife.OnClick; |
||||
|
|
||||
|
/** |
||||
|
* AndroidUSBCamera引擎使用Demo |
||||
|
* |
||||
|
* Created by jiangdongguo on 2017/9/30. |
||||
|
*/ |
||||
|
|
||||
|
public class USBCameraActivity extends AppCompatActivity implements CameraDialog.CameraDialogParent{ |
||||
|
@BindView(R.id.camera_view) |
||||
|
public View mTextureView; |
||||
|
@BindView(R.id.btn_capture_pic) |
||||
|
public Button mBtnCapture; |
||||
|
@BindView(R.id.btn_rec_video) |
||||
|
public Button mBtnRecord; |
||||
|
|
||||
|
private USBCameraManager mUSBManager; |
||||
|
private CameraViewInterface mUVCCameraView; |
||||
|
|
||||
|
// USB设备监听器
|
||||
|
private USBCameraManager.OnMyDevConnectListener listener = new USBCameraManager.OnMyDevConnectListener() { |
||||
|
@Override |
||||
|
public void onAttachDev(UsbDevice device) { |
||||
|
showShortMsg("检测到设备:"+device.getDeviceName()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDettachDev(UsbDevice device) { |
||||
|
showShortMsg(device.getDeviceName()+"已拨出"); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onConnectDev(UsbDevice device) { |
||||
|
// 处理连接到设备后的逻辑
|
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDisConnectDev(UsbDevice device) { |
||||
|
// 处理与设备断开后的逻辑
|
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
@Override |
||||
|
protected void onCreate(@Nullable Bundle savedInstanceState) { |
||||
|
super.onCreate(savedInstanceState); |
||||
|
setContentView(R.layout.activity_usbcamera); |
||||
|
ButterKnife.bind(this); |
||||
|
// 初始化引擎
|
||||
|
mUSBManager = USBCameraManager.getInstance(); |
||||
|
mUVCCameraView = (CameraViewInterface) mTextureView; |
||||
|
mUSBManager.init(this, mUVCCameraView, listener); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onStart() { |
||||
|
super.onStart(); |
||||
|
// 注册USB事件广播监听器
|
||||
|
if(mUSBManager != null){ |
||||
|
mUSBManager.registerUSB(); |
||||
|
} |
||||
|
// 恢复Camera预览
|
||||
|
if(mUVCCameraView != null){ |
||||
|
mUVCCameraView.onResume(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onStop() { |
||||
|
super.onStop(); |
||||
|
// 注销USB事件广播监听器
|
||||
|
if(mUSBManager != null){ |
||||
|
mUSBManager.unregisterUSB(); |
||||
|
} |
||||
|
// 暂停Camera预览
|
||||
|
if(mUVCCameraView != null){ |
||||
|
mUVCCameraView.onPause(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@OnClick({R.id.camera_view, R.id.btn_capture_pic, R.id.btn_rec_video}) |
||||
|
public void onViewClick(View view) { |
||||
|
int vId = view.getId(); |
||||
|
switch (vId) { |
||||
|
// 开启或关闭Camera
|
||||
|
case R.id.camera_view: |
||||
|
if(mUSBManager != null){ |
||||
|
boolean isOpened = mUSBManager.isCameraOpened(); |
||||
|
if(! isOpened){ |
||||
|
CameraDialog.showDialog(USBCameraActivity.this); |
||||
|
}else { |
||||
|
mUSBManager.closeCamera(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
break; |
||||
|
case R.id.btn_capture_pic: |
||||
|
if(mUSBManager == null || ! mUSBManager.isCameraOpened()){ |
||||
|
showShortMsg("抓拍异常,摄像头未开启"); |
||||
|
return; |
||||
|
} |
||||
|
String picPath = USBCameraManager.ROOT_PATH+System.currentTimeMillis() |
||||
|
+USBCameraManager.SUFFIX_PNG; |
||||
|
mUSBManager.capturePicture(picPath); |
||||
|
|
||||
|
showShortMsg("保存路径:"+picPath); |
||||
|
break; |
||||
|
case R.id.btn_rec_video: |
||||
|
if(mUSBManager == null || ! mUSBManager.isCameraOpened()){ |
||||
|
showShortMsg("录制异常,摄像头未开启"); |
||||
|
return; |
||||
|
} |
||||
|
if(! mUSBManager.isRecording()){ |
||||
|
String videoPath = USBCameraManager.ROOT_PATH+System.currentTimeMillis() |
||||
|
+USBCameraManager.SUFFIX_MP4; |
||||
|
mUSBManager.startRecording(videoPath); |
||||
|
|
||||
|
mBtnRecord.setText("正在录制"); |
||||
|
} else { |
||||
|
mUSBManager.stopRecording(); |
||||
|
|
||||
|
mBtnRecord.setText("开始录制"); |
||||
|
} |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void onDestroy() { |
||||
|
super.onDestroy(); |
||||
|
// 释放资源
|
||||
|
if(mUSBManager != null){ |
||||
|
mUSBManager.release(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void showShortMsg(String msg) { |
||||
|
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public USBMonitor getUSBMonitor() { |
||||
|
return mUSBManager.getUSBMonitor(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onDialogResult(boolean canceled) { |
||||
|
if(canceled){ |
||||
|
showShortMsg("取消操作"); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -1,15 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
|
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||
xmlns:tools="http://schemas.android.com/tools" |
|
||||
android:id="@+id/container" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent" |
|
||||
tools:ignore="MergeRootFrame" > |
|
||||
|
|
||||
<SurfaceView |
|
||||
android:id="@+id/camera_surface_view" |
|
||||
android:layout_width="match_parent" |
|
||||
android:layout_height="match_parent" /> |
|
||||
|
|
||||
</FrameLayout> |
|
@ -0,0 +1,39 @@ |
|||||
|
<?xml version="1.0" encoding="utf-8"?> |
||||
|
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
xmlns:tools="http://schemas.android.com/tools" |
||||
|
android:background="#ff000000" |
||||
|
tools:context=".view.USBCameraActivity" |
||||
|
tools:ignore="MergeRootFrame"> |
||||
|
|
||||
|
<com.serenegiant.usb.widget.UVCCameraTextureView |
||||
|
android:id="@+id/camera_view" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="match_parent" |
||||
|
android:layout_centerVertical="true" |
||||
|
android:layout_centerHorizontal="true"/> |
||||
|
|
||||
|
<Button |
||||
|
android:id="@+id/btn_capture_pic" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_alignParentBottom="true" |
||||
|
android:layout_marginBottom="10dp" |
||||
|
android:layout_marginRight="10dp" |
||||
|
android:layout_marginLeft="10dp" |
||||
|
android:textSize="16sp" |
||||
|
android:text="抓拍"/> |
||||
|
|
||||
|
<Button |
||||
|
android:layout_above="@id/btn_capture_pic" |
||||
|
android:id="@+id/btn_rec_video" |
||||
|
android:layout_width="match_parent" |
||||
|
android:layout_height="wrap_content" |
||||
|
android:layout_marginRight="10dp" |
||||
|
android:layout_marginLeft="10dp" |
||||
|
android:textSize="16sp" |
||||
|
android:text="开始录制"/> |
||||
|
|
||||
|
|
||||
|
</RelativeLayout> |
@ -0,0 +1,207 @@ |
|||||
|
package com.jiangdg.usbcamera; |
||||
|
|
||||
|
import android.app.Activity; |
||||
|
import android.graphics.SurfaceTexture; |
||||
|
import android.hardware.usb.UsbDevice; |
||||
|
import android.os.Environment; |
||||
|
|
||||
|
import com.serenegiant.usb.CameraDialog; |
||||
|
import com.serenegiant.usb.USBMonitor; |
||||
|
import com.serenegiant.usb.common.UVCCameraHandler; |
||||
|
import com.serenegiant.usb.widget.CameraViewInterface; |
||||
|
|
||||
|
import java.io.File; |
||||
|
|
||||
|
/**USB摄像头工具类 |
||||
|
* |
||||
|
* Created by jiangdongguo on 2017/9/30. |
||||
|
*/ |
||||
|
|
||||
|
public class USBCameraManager{ |
||||
|
public static final String ROOT_PATH = Environment.getExternalStorageDirectory().getAbsolutePath() |
||||
|
+ File.separator; |
||||
|
public static final String SUFFIX_PNG = ".png"; |
||||
|
public static final String SUFFIX_MP4 = ".mp4"; |
||||
|
private static final String TAG = "USBCameraManager"; |
||||
|
private static final int PREVIEW_WIDTH = 640; |
||||
|
private static final int PREVIEW_HEIGHT = 480; |
||||
|
private static final int ENCODER_TYPE = 1; |
||||
|
//0为YUYV,1为MJPEG
|
||||
|
private static final int PREVIEW_FORMAT = 1; |
||||
|
|
||||
|
private static USBCameraManager mUsbCamManager; |
||||
|
// USB设备管理类
|
||||
|
private USBMonitor mUSBMonitor; |
||||
|
// Camera业务逻辑处理
|
||||
|
private UVCCameraHandler mCameraHandler; |
||||
|
|
||||
|
private USBCameraManager(){} |
||||
|
|
||||
|
public static USBCameraManager getInstance(){ |
||||
|
if(mUsbCamManager == null){ |
||||
|
mUsbCamManager = new USBCameraManager(); |
||||
|
} |
||||
|
return mUsbCamManager; |
||||
|
} |
||||
|
|
||||
|
public interface OnMyDevConnectListener{ |
||||
|
void onAttachDev(UsbDevice device); |
||||
|
void onDettachDev(UsbDevice device); |
||||
|
void onConnectDev(UsbDevice device); |
||||
|
void onDisConnectDev(UsbDevice device); |
||||
|
} |
||||
|
|
||||
|
/** 初始化 |
||||
|
* |
||||
|
* context 上下文 |
||||
|
* cameraView Camera要渲染的Surface |
||||
|
* listener USB设备检测与连接状态事件监听器 |
||||
|
* */ |
||||
|
public void init(Activity activity, final CameraViewInterface cameraView, final OnMyDevConnectListener listener){ |
||||
|
if(cameraView == null) |
||||
|
throw new NullPointerException("CameraViewInterface cannot be null!"); |
||||
|
mUSBMonitor = new USBMonitor(activity.getApplicationContext(), new USBMonitor.OnDeviceConnectListener() { |
||||
|
// 当检测到USB设备,被回调
|
||||
|
@Override |
||||
|
public void onAttach(UsbDevice device) { |
||||
|
if(listener != null){ |
||||
|
listener.onAttachDev(device); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 当拨出或未检测到USB设备,被回调
|
||||
|
@Override |
||||
|
public void onDettach(UsbDevice device) { |
||||
|
if(listener != null){ |
||||
|
listener.onDettachDev(device); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
// 当连接到USB Camera时,被回调
|
||||
|
@Override |
||||
|
public void onConnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock, boolean createNew) { |
||||
|
if(listener != null){ |
||||
|
listener.onConnectDev(device); |
||||
|
} |
||||
|
// 打开摄像头
|
||||
|
openCamera(ctrlBlock); |
||||
|
// 开启预览
|
||||
|
startPreview(cameraView); |
||||
|
} |
||||
|
|
||||
|
// 当与USB Camera断开连接时,被回调
|
||||
|
@Override |
||||
|
public void onDisconnect(UsbDevice device, USBMonitor.UsbControlBlock ctrlBlock) { |
||||
|
if(listener != null){ |
||||
|
listener.onDisConnectDev(device); |
||||
|
} |
||||
|
// 关闭摄像头
|
||||
|
closeCamera(); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
public void onCancel(UsbDevice device) { |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// 设置长宽比
|
||||
|
cameraView.setAspectRatio(PREVIEW_WIDTH / (float)PREVIEW_HEIGHT); |
||||
|
mCameraHandler = UVCCameraHandler.createHandler(activity,cameraView,ENCODER_TYPE, |
||||
|
PREVIEW_WIDTH,PREVIEW_HEIGHT,PREVIEW_FORMAT); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注册检测USB设备广播接收器 |
||||
|
* */ |
||||
|
public void registerUSB(){ |
||||
|
if(mUSBMonitor != null){ |
||||
|
mUSBMonitor.register(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 注销检测USB设备广播接收器 |
||||
|
*/ |
||||
|
public void unregisterUSB(){ |
||||
|
if(mUSBMonitor != null){ |
||||
|
mUSBMonitor.unregister(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 抓拍照片 |
||||
|
* */ |
||||
|
public void capturePicture(String savePath){ |
||||
|
if(mCameraHandler != null && mCameraHandler.isOpened()){ |
||||
|
mCameraHandler.captureStill(savePath); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void startRecording(String videoPath){ |
||||
|
if(mCameraHandler != null && ! isRecording()){ |
||||
|
mCameraHandler.startRecording(videoPath); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void stopRecording(){ |
||||
|
if(mCameraHandler != null && isRecording()){ |
||||
|
mCameraHandler.stopRecording(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public boolean isRecording(){ |
||||
|
if(mCameraHandler != null){ |
||||
|
return mCameraHandler.isRecording(); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
public boolean isCameraOpened(){ |
||||
|
if(mCameraHandler != null){ |
||||
|
return mCameraHandler.isOpened(); |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
|
||||
|
/** |
||||
|
* 释放资源 |
||||
|
* */ |
||||
|
public void release(){ |
||||
|
//释放CameraHandler占用的相关资源
|
||||
|
if(mCameraHandler != null){ |
||||
|
mCameraHandler.release(); |
||||
|
mCameraHandler = null; |
||||
|
} |
||||
|
// 释放USBMonitor占用的相关资源
|
||||
|
if(mUSBMonitor != null){ |
||||
|
mUSBMonitor.destroy(); |
||||
|
mUSBMonitor = null; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public USBMonitor getUSBMonitor() { |
||||
|
return mUSBMonitor; |
||||
|
} |
||||
|
|
||||
|
|
||||
|
public void closeCamera() { |
||||
|
if(mCameraHandler != null){ |
||||
|
mCameraHandler.close(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void openCamera(USBMonitor.UsbControlBlock ctrlBlock) { |
||||
|
if(mCameraHandler != null){ |
||||
|
mCameraHandler.open(ctrlBlock); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private void startPreview(CameraViewInterface cameraView) { |
||||
|
SurfaceTexture st = cameraView.getSurfaceTexture(); |
||||
|
if(mCameraHandler != null){ |
||||
|
mCameraHandler.startPreview(st); |
||||
|
} |
||||
|
} |
||||
|
} |
@ -0,0 +1,27 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
public interface IAudioEncoder { |
||||
|
} |
@ -0,0 +1,28 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
public interface IVideoEncoder { |
||||
|
public boolean frameAvailableSoon(); |
||||
|
} |
@ -0,0 +1,233 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.AudioFormat; |
||||
|
import android.media.AudioRecord; |
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaCodecInfo; |
||||
|
import android.media.MediaCodecList; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.media.MediaRecorder; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.nio.ByteBuffer; |
||||
|
import java.nio.ByteOrder; |
||||
|
|
||||
|
public class MediaAudioEncoder extends MediaEncoder implements IAudioEncoder { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaAudioEncoder"; |
||||
|
|
||||
|
private static final String MIME_TYPE = "audio/mp4a-latm"; |
||||
|
private static final int SAMPLE_RATE = 44100; // 44.1[KHz] is only setting guaranteed to be available on all devices.
|
||||
|
private static final int BIT_RATE = 64000; |
||||
|
public static final int SAMPLES_PER_FRAME = 1024; // AAC, bytes/frame/channel
|
||||
|
public static final int FRAMES_PER_BUFFER = 25; // AAC, frame/buffer/sec
|
||||
|
|
||||
|
private AudioThread mAudioThread = null; |
||||
|
|
||||
|
public MediaAudioEncoder(final MediaMuxerWrapper muxer, final MediaEncoderListener listener) { |
||||
|
super(muxer, listener); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void prepare() throws IOException { |
||||
|
if (DEBUG) Log.v(TAG, "prepare:"); |
||||
|
mTrackIndex = -1; |
||||
|
mMuxerStarted = mIsEOS = false; |
||||
|
// prepare MediaCodec for AAC encoding of audio data from inernal mic.
|
||||
|
final MediaCodecInfo audioCodecInfo = selectAudioCodec(MIME_TYPE); |
||||
|
if (audioCodecInfo == null) { |
||||
|
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
||||
|
return; |
||||
|
} |
||||
|
if (DEBUG) Log.i(TAG, "selected codec: " + audioCodecInfo.getName()); |
||||
|
|
||||
|
final MediaFormat audioFormat = MediaFormat.createAudioFormat(MIME_TYPE, SAMPLE_RATE, 1); |
||||
|
audioFormat.setInteger(MediaFormat.KEY_AAC_PROFILE, MediaCodecInfo.CodecProfileLevel.AACObjectLC); |
||||
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_MASK, AudioFormat.CHANNEL_IN_MONO); |
||||
|
audioFormat.setInteger(MediaFormat.KEY_BIT_RATE, BIT_RATE); |
||||
|
audioFormat.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); |
||||
|
// audioFormat.setLong(MediaFormat.KEY_MAX_INPUT_SIZE, inputFile.length());
|
||||
|
// audioFormat.setLong(MediaFormat.KEY_DURATION, (long)durationInMs );
|
||||
|
if (DEBUG) Log.i(TAG, "format: " + audioFormat); |
||||
|
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
||||
|
mMediaCodec.configure(audioFormat, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
||||
|
mMediaCodec.start(); |
||||
|
if (DEBUG) Log.i(TAG, "prepare finishing"); |
||||
|
if (mListener != null) { |
||||
|
try { |
||||
|
mListener.onPrepared(this); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "prepare:", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void startRecording() { |
||||
|
super.startRecording(); |
||||
|
// create and execute audio capturing thread using internal mic
|
||||
|
if (mAudioThread == null) { |
||||
|
mAudioThread = new AudioThread(); |
||||
|
mAudioThread.start(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void release() { |
||||
|
mAudioThread = null; |
||||
|
super.release(); |
||||
|
} |
||||
|
|
||||
|
private static final int[] AUDIO_SOURCES = new int[] { |
||||
|
MediaRecorder.AudioSource.DEFAULT, |
||||
|
MediaRecorder.AudioSource.MIC, |
||||
|
MediaRecorder.AudioSource.CAMCORDER, |
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Thread to capture audio data from internal mic as uncompressed 16bit PCM data |
||||
|
* and write them to the MediaCodec encoder |
||||
|
*/ |
||||
|
private class AudioThread extends Thread { |
||||
|
@Override |
||||
|
public void run() { |
||||
|
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_AUDIO); // THREAD_PRIORITY_URGENT_AUDIO
|
||||
|
int cnt = 0; |
||||
|
final int min_buffer_size = AudioRecord.getMinBufferSize( |
||||
|
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT); |
||||
|
int buffer_size = SAMPLES_PER_FRAME * FRAMES_PER_BUFFER; |
||||
|
if (buffer_size < min_buffer_size) |
||||
|
buffer_size = ((min_buffer_size / SAMPLES_PER_FRAME) + 1) * SAMPLES_PER_FRAME * 2; |
||||
|
final ByteBuffer buf = ByteBuffer.allocateDirect(SAMPLES_PER_FRAME).order(ByteOrder.nativeOrder()); |
||||
|
AudioRecord audioRecord = null; |
||||
|
for (final int src: AUDIO_SOURCES) { |
||||
|
try { |
||||
|
audioRecord = new AudioRecord(src, |
||||
|
SAMPLE_RATE, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT, buffer_size); |
||||
|
if (audioRecord != null) { |
||||
|
if (audioRecord.getState() != AudioRecord.STATE_INITIALIZED) { |
||||
|
audioRecord.release(); |
||||
|
audioRecord = null; |
||||
|
} |
||||
|
} |
||||
|
} catch (final Exception e) { |
||||
|
audioRecord = null; |
||||
|
} |
||||
|
if (audioRecord != null) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (audioRecord != null) { |
||||
|
try { |
||||
|
if (mIsCapturing) { |
||||
|
if (DEBUG) Log.v(TAG, "AudioThread:start audio recording"); |
||||
|
int readBytes; |
||||
|
audioRecord.startRecording(); |
||||
|
try { |
||||
|
for ( ; mIsCapturing && !mRequestStop && !mIsEOS ; ) { |
||||
|
// read audio data from internal mic
|
||||
|
buf.clear(); |
||||
|
try { |
||||
|
readBytes = audioRecord.read(buf, SAMPLES_PER_FRAME); |
||||
|
} catch (final Exception e) { |
||||
|
break; |
||||
|
} |
||||
|
if (readBytes > 0) { |
||||
|
// set audio data to encoder
|
||||
|
buf.position(readBytes); |
||||
|
buf.flip(); |
||||
|
encode(buf, readBytes, getPTSUs()); |
||||
|
frameAvailableSoon(); |
||||
|
cnt++; |
||||
|
} |
||||
|
} |
||||
|
if (cnt > 0) { |
||||
|
frameAvailableSoon(); |
||||
|
} |
||||
|
} finally { |
||||
|
audioRecord.stop(); |
||||
|
} |
||||
|
} |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "AudioThread#run", e); |
||||
|
} finally { |
||||
|
audioRecord.release(); |
||||
|
} |
||||
|
} |
||||
|
if (cnt == 0) { |
||||
|
for (int i = 0; mIsCapturing && (i < 5); i++) { |
||||
|
buf.position(SAMPLES_PER_FRAME); |
||||
|
buf.flip(); |
||||
|
try { |
||||
|
encode(buf, SAMPLES_PER_FRAME, getPTSUs()); |
||||
|
frameAvailableSoon(); |
||||
|
} catch (final Exception e) { |
||||
|
break; |
||||
|
} |
||||
|
synchronized(this) { |
||||
|
try { |
||||
|
wait(50); |
||||
|
} catch (final InterruptedException e) { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
if (DEBUG) Log.v(TAG, "AudioThread:finished"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select the first codec that match a specific MIME type |
||||
|
* @param mimeType |
||||
|
* @return |
||||
|
*/ |
||||
|
private static final MediaCodecInfo selectAudioCodec(final String mimeType) { |
||||
|
if (DEBUG) Log.v(TAG, "selectAudioCodec:"); |
||||
|
|
||||
|
MediaCodecInfo result = null; |
||||
|
// get the list of available codecs
|
||||
|
final int numCodecs = MediaCodecList.getCodecCount(); |
||||
|
LOOP: for (int i = 0; i < numCodecs; i++) { |
||||
|
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
||||
|
if (!codecInfo.isEncoder()) { // skipp decoder
|
||||
|
continue; |
||||
|
} |
||||
|
final String[] types = codecInfo.getSupportedTypes(); |
||||
|
for (int j = 0; j < types.length; j++) { |
||||
|
if (DEBUG) Log.i(TAG, "supportedType:" + codecInfo.getName() + ",MIME=" + types[j]); |
||||
|
if (types[j].equalsIgnoreCase(mimeType)) { |
||||
|
if (result == null) { |
||||
|
result = codecInfo; |
||||
|
break LOOP; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,448 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.lang.ref.WeakReference; |
||||
|
import java.nio.ByteBuffer; |
||||
|
|
||||
|
public abstract class MediaEncoder implements Runnable { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaEncoder"; |
||||
|
|
||||
|
protected static final int TIMEOUT_USEC = 10000; // 10[msec]
|
||||
|
protected static final int MSG_FRAME_AVAILABLE = 1; |
||||
|
protected static final int MSG_STOP_RECORDING = 9; |
||||
|
|
||||
|
public interface MediaEncoderListener { |
||||
|
public void onPrepared(MediaEncoder encoder); |
||||
|
public void onStopped(MediaEncoder encoder); |
||||
|
} |
||||
|
|
||||
|
protected final Object mSync = new Object(); |
||||
|
/** |
||||
|
* Flag that indicate this encoder is capturing now. |
||||
|
*/ |
||||
|
protected volatile boolean mIsCapturing; |
||||
|
/** |
||||
|
* Flag that indicate the frame data will be available soon. |
||||
|
*/ |
||||
|
private int mRequestDrain; |
||||
|
/** |
||||
|
* Flag to request stop capturing |
||||
|
*/ |
||||
|
protected volatile boolean mRequestStop; |
||||
|
/** |
||||
|
* Flag that indicate encoder received EOS(End Of Stream) |
||||
|
*/ |
||||
|
protected boolean mIsEOS; |
||||
|
/** |
||||
|
* Flag the indicate the muxer is running |
||||
|
*/ |
||||
|
protected boolean mMuxerStarted; |
||||
|
/** |
||||
|
* Track Number |
||||
|
*/ |
||||
|
protected int mTrackIndex; |
||||
|
/** |
||||
|
* MediaCodec instance for encoding |
||||
|
*/ |
||||
|
protected MediaCodec mMediaCodec; // API >= 16(Android4.1.2)
|
||||
|
/** |
||||
|
* Weak refarence of MediaMuxerWarapper instance |
||||
|
*/ |
||||
|
protected final WeakReference<MediaMuxerWrapper> mWeakMuxer; |
||||
|
/** |
||||
|
* BufferInfo instance for dequeuing |
||||
|
*/ |
||||
|
private MediaCodec.BufferInfo mBufferInfo; // API >= 16(Android4.1.2)
|
||||
|
|
||||
|
protected final MediaEncoderListener mListener; |
||||
|
|
||||
|
public MediaEncoder(final MediaMuxerWrapper muxer, final MediaEncoderListener listener) { |
||||
|
if (listener == null) throw new NullPointerException("MediaEncoderListener is null"); |
||||
|
if (muxer == null) throw new NullPointerException("MediaMuxerWrapper is null"); |
||||
|
mWeakMuxer = new WeakReference<MediaMuxerWrapper>(muxer); |
||||
|
muxer.addEncoder(this); |
||||
|
mListener = listener; |
||||
|
synchronized (mSync) { |
||||
|
// create BufferInfo here for effectiveness(to reduce GC)
|
||||
|
mBufferInfo = new MediaCodec.BufferInfo(); |
||||
|
// wait for starting thread
|
||||
|
new Thread(this, getClass().getSimpleName()).start(); |
||||
|
try { |
||||
|
mSync.wait(); |
||||
|
} catch (final InterruptedException e) { |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public String getOutputPath() { |
||||
|
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
||||
|
return muxer != null ? muxer.getOutputPath() : null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* the method to indicate frame data is soon available or already available |
||||
|
* @return return true if encoder is ready to encod. |
||||
|
*/ |
||||
|
public boolean frameAvailableSoon() { |
||||
|
// if (DEBUG) Log.v(TAG, "frameAvailableSoon");
|
||||
|
synchronized (mSync) { |
||||
|
if (!mIsCapturing || mRequestStop) { |
||||
|
return false; |
||||
|
} |
||||
|
mRequestDrain++; |
||||
|
mSync.notifyAll(); |
||||
|
} |
||||
|
return true; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* encoding loop on private thread |
||||
|
*/ |
||||
|
@Override |
||||
|
public void run() { |
||||
|
// android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);
|
||||
|
synchronized (mSync) { |
||||
|
mRequestStop = false; |
||||
|
mRequestDrain = 0; |
||||
|
mSync.notify(); |
||||
|
} |
||||
|
final boolean isRunning = true; |
||||
|
boolean localRequestStop; |
||||
|
boolean localRequestDrain; |
||||
|
while (isRunning) { |
||||
|
synchronized (mSync) { |
||||
|
localRequestStop = mRequestStop; |
||||
|
localRequestDrain = (mRequestDrain > 0); |
||||
|
if (localRequestDrain) |
||||
|
mRequestDrain--; |
||||
|
} |
||||
|
if (localRequestStop) { |
||||
|
drain(); |
||||
|
// request stop recording
|
||||
|
signalEndOfInputStream(); |
||||
|
// process output data again for EOS signale
|
||||
|
drain(); |
||||
|
// release all related objects
|
||||
|
release(); |
||||
|
break; |
||||
|
} |
||||
|
if (localRequestDrain) { |
||||
|
drain(); |
||||
|
} else { |
||||
|
synchronized (mSync) { |
||||
|
try { |
||||
|
mSync.wait(); |
||||
|
} catch (final InterruptedException e) { |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} // end of while
|
||||
|
if (DEBUG) Log.d(TAG, "Encoder thread exiting"); |
||||
|
synchronized (mSync) { |
||||
|
mRequestStop = true; |
||||
|
mIsCapturing = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/* |
||||
|
* prepareing method for each sub class |
||||
|
* this method should be implemented in sub class, so set this as abstract method |
||||
|
* @throws IOException |
||||
|
*/ |
||||
|
/*package*/ abstract void prepare() throws IOException; |
||||
|
|
||||
|
/*package*/ void startRecording() { |
||||
|
if (DEBUG) Log.v(TAG, "startRecording"); |
||||
|
synchronized (mSync) { |
||||
|
mIsCapturing = true; |
||||
|
mRequestStop = false; |
||||
|
mSync.notifyAll(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* the method to request stop encoding |
||||
|
*/ |
||||
|
/*package*/ void stopRecording() { |
||||
|
if (DEBUG) Log.v(TAG, "stopRecording"); |
||||
|
synchronized (mSync) { |
||||
|
if (!mIsCapturing || mRequestStop) { |
||||
|
return; |
||||
|
} |
||||
|
mRequestStop = true; // for rejecting newer frame
|
||||
|
mSync.notifyAll(); |
||||
|
// We can not know when the encoding and writing finish.
|
||||
|
// so we return immediately after request to avoid delay of caller thread
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
//********************************************************************************
|
||||
|
//********************************************************************************
|
||||
|
/** |
||||
|
* Release all releated objects |
||||
|
*/ |
||||
|
protected void release() { |
||||
|
if (DEBUG) Log.d(TAG, "release:"); |
||||
|
try { |
||||
|
mListener.onStopped(this); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "failed onStopped", e); |
||||
|
} |
||||
|
mIsCapturing = false; |
||||
|
if (mMediaCodec != null) { |
||||
|
try { |
||||
|
mMediaCodec.stop(); |
||||
|
mMediaCodec.release(); |
||||
|
mMediaCodec = null; |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "failed releasing MediaCodec", e); |
||||
|
} |
||||
|
} |
||||
|
if (mMuxerStarted) { |
||||
|
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
||||
|
if (muxer != null) { |
||||
|
try { |
||||
|
muxer.stop(); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "failed stopping muxer", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
mBufferInfo = null; |
||||
|
} |
||||
|
|
||||
|
protected void signalEndOfInputStream() { |
||||
|
if (DEBUG) Log.d(TAG, "sending EOS to encoder"); |
||||
|
// signalEndOfInputStream is only avairable for video encoding with surface
|
||||
|
// and equivalent sending a empty buffer with BUFFER_FLAG_END_OF_STREAM flag.
|
||||
|
// mMediaCodec.signalEndOfInputStream(); // API >= 18
|
||||
|
encode((byte[])null, 0, getPTSUs()); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Method to set byte array to the MediaCodec encoder |
||||
|
* @param buffer |
||||
|
* @param length length of byte array, zero means EOS. |
||||
|
* @param presentationTimeUs |
||||
|
*/ |
||||
|
@SuppressWarnings("deprecation") |
||||
|
protected void encode(final byte[] buffer, final int length, final long presentationTimeUs) { |
||||
|
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
||||
|
if (!mIsCapturing) return; |
||||
|
int ix = 0, sz; |
||||
|
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); |
||||
|
while (mIsCapturing && ix < length) { |
||||
|
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); |
||||
|
if (inputBufferIndex >= 0) { |
||||
|
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; |
||||
|
inputBuffer.clear(); |
||||
|
sz = inputBuffer.remaining(); |
||||
|
sz = (ix + sz < length) ? sz : length - ix; |
||||
|
if (sz > 0 && (buffer != null)) { |
||||
|
inputBuffer.put(buffer, ix, sz); |
||||
|
} |
||||
|
ix += sz; |
||||
|
// if (DEBUG) Log.v(TAG, "encode:queueInputBuffer");
|
||||
|
if (length <= 0) { |
||||
|
// send EOS
|
||||
|
mIsEOS = true; |
||||
|
if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM"); |
||||
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, |
||||
|
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
||||
|
break; |
||||
|
} else { |
||||
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz, |
||||
|
presentationTimeUs, 0); |
||||
|
} |
||||
|
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { |
||||
|
// wait for MediaCodec encoder is ready to encode
|
||||
|
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
|
||||
|
// will wait for maximum TIMEOUT_USEC(10msec) on each call
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Method to set ByteBuffer to the MediaCodec encoder |
||||
|
* @param buffer null means EOS |
||||
|
* @param presentationTimeUs |
||||
|
*/ |
||||
|
@SuppressWarnings("deprecation") |
||||
|
protected void encode(final ByteBuffer buffer, final int length, final long presentationTimeUs) { |
||||
|
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
||||
|
if (!mIsCapturing) return; |
||||
|
int ix = 0, sz; |
||||
|
final ByteBuffer[] inputBuffers = mMediaCodec.getInputBuffers(); |
||||
|
while (mIsCapturing && ix < length) { |
||||
|
final int inputBufferIndex = mMediaCodec.dequeueInputBuffer(TIMEOUT_USEC); |
||||
|
if (inputBufferIndex >= 0) { |
||||
|
final ByteBuffer inputBuffer = inputBuffers[inputBufferIndex]; |
||||
|
inputBuffer.clear(); |
||||
|
sz = inputBuffer.remaining(); |
||||
|
sz = (ix + sz < length) ? sz : length - ix; |
||||
|
if (sz > 0 && (buffer != null)) { |
||||
|
buffer.position(ix + sz); |
||||
|
buffer.flip(); |
||||
|
inputBuffer.put(buffer); |
||||
|
} |
||||
|
ix += sz; |
||||
|
// if (DEBUG) Log.v(TAG, "encode:queueInputBuffer");
|
||||
|
if (length <= 0) { |
||||
|
// send EOS
|
||||
|
mIsEOS = true; |
||||
|
if (DEBUG) Log.i(TAG, "send BUFFER_FLAG_END_OF_STREAM"); |
||||
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, 0, |
||||
|
presentationTimeUs, MediaCodec.BUFFER_FLAG_END_OF_STREAM); |
||||
|
break; |
||||
|
} else { |
||||
|
mMediaCodec.queueInputBuffer(inputBufferIndex, 0, sz, |
||||
|
presentationTimeUs, 0); |
||||
|
} |
||||
|
} else if (inputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER) { |
||||
|
// wait for MediaCodec encoder is ready to encode
|
||||
|
// nothing to do here because MediaCodec#dequeueInputBuffer(TIMEOUT_USEC)
|
||||
|
// will wait for maximum TIMEOUT_USEC(10msec) on each call
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* drain encoded data and write them to muxer |
||||
|
*/ |
||||
|
@SuppressWarnings("deprecation") |
||||
|
protected void drain() { |
||||
|
if (mMediaCodec == null) return; |
||||
|
ByteBuffer[] encoderOutputBuffers = mMediaCodec.getOutputBuffers(); |
||||
|
int encoderStatus, count = 0; |
||||
|
final MediaMuxerWrapper muxer = mWeakMuxer.get(); |
||||
|
if (muxer == null) { |
||||
|
// throw new NullPointerException("muxer is unexpectedly null");
|
||||
|
Log.w(TAG, "muxer is unexpectedly null"); |
||||
|
return; |
||||
|
} |
||||
|
LOOP: while (mIsCapturing) { |
||||
|
// get encoded data with maximum timeout duration of TIMEOUT_USEC(=10[msec])
|
||||
|
encoderStatus = mMediaCodec.dequeueOutputBuffer(mBufferInfo, TIMEOUT_USEC); |
||||
|
if (encoderStatus == MediaCodec.INFO_TRY_AGAIN_LATER) { |
||||
|
// wait 5 counts(=TIMEOUT_USEC x 5 = 50msec) until data/EOS come
|
||||
|
if (!mIsEOS) { |
||||
|
if (++count > 5) |
||||
|
break LOOP; // out of while
|
||||
|
} |
||||
|
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { |
||||
|
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_BUFFERS_CHANGED"); |
||||
|
// this shoud not come when encoding
|
||||
|
encoderOutputBuffers = mMediaCodec.getOutputBuffers(); |
||||
|
} else if (encoderStatus == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { |
||||
|
if (DEBUG) Log.v(TAG, "INFO_OUTPUT_FORMAT_CHANGED"); |
||||
|
// this status indicate the output format of codec is changed
|
||||
|
// this should come only once before actual encoded data
|
||||
|
// but this status never come on Android4.3 or less
|
||||
|
// and in that case, you should treat when MediaCodec.BUFFER_FLAG_CODEC_CONFIG come.
|
||||
|
if (mMuxerStarted) { // second time request is error
|
||||
|
throw new RuntimeException("format changed twice"); |
||||
|
} |
||||
|
// get output format from codec and pass them to muxer
|
||||
|
// getOutputFormat should be called after INFO_OUTPUT_FORMAT_CHANGED otherwise crash.
|
||||
|
final MediaFormat format = mMediaCodec.getOutputFormat(); // API >= 16
|
||||
|
mTrackIndex = muxer.addTrack(format); |
||||
|
mMuxerStarted = true; |
||||
|
if (!muxer.start()) { |
||||
|
// we should wait until muxer is ready
|
||||
|
synchronized (muxer) { |
||||
|
while (!muxer.isStarted()) |
||||
|
try { |
||||
|
muxer.wait(100); |
||||
|
} catch (final InterruptedException e) { |
||||
|
break LOOP; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} else if (encoderStatus < 0) { |
||||
|
// unexpected status
|
||||
|
if (DEBUG) Log.w(TAG, "drain:unexpected result from encoder#dequeueOutputBuffer: " + encoderStatus); |
||||
|
} else { |
||||
|
final ByteBuffer encodedData = encoderOutputBuffers[encoderStatus]; |
||||
|
if (encodedData == null) { |
||||
|
// this never should come...may be a MediaCodec internal error
|
||||
|
throw new RuntimeException("encoderOutputBuffer " + encoderStatus + " was null"); |
||||
|
} |
||||
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { |
||||
|
// You shoud set output format to muxer here when you target Android4.3 or less
|
||||
|
// but MediaCodec#getOutputFormat can not call here(because INFO_OUTPUT_FORMAT_CHANGED don't come yet)
|
||||
|
// therefor we should expand and prepare output format from buffer data.
|
||||
|
// This sample is for API>=18(>=Android 4.3), just ignore this flag here
|
||||
|
if (DEBUG) Log.d(TAG, "drain:BUFFER_FLAG_CODEC_CONFIG"); |
||||
|
mBufferInfo.size = 0; |
||||
|
} |
||||
|
|
||||
|
if (mBufferInfo.size != 0) { |
||||
|
// encoded data is ready, clear waiting counter
|
||||
|
count = 0; |
||||
|
if (!mMuxerStarted) { |
||||
|
// muxer is not ready...this will prrograming failure.
|
||||
|
throw new RuntimeException("drain:muxer hasn't started"); |
||||
|
} |
||||
|
// write encoded data to muxer(need to adjust presentationTimeUs.
|
||||
|
mBufferInfo.presentationTimeUs = getPTSUs(); |
||||
|
muxer.writeSampleData(mTrackIndex, encodedData, mBufferInfo); |
||||
|
prevOutputPTSUs = mBufferInfo.presentationTimeUs; |
||||
|
} |
||||
|
// return buffer to encoder
|
||||
|
mMediaCodec.releaseOutputBuffer(encoderStatus, false); |
||||
|
if ((mBufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { |
||||
|
// when EOS come.
|
||||
|
mMuxerStarted = mIsCapturing = false; |
||||
|
break; // out of while
|
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* previous presentationTimeUs for writing |
||||
|
*/ |
||||
|
private long prevOutputPTSUs = 0; |
||||
|
/** |
||||
|
* get next encoding presentationTimeUs |
||||
|
* @return |
||||
|
*/ |
||||
|
protected long getPTSUs() { |
||||
|
long result = System.nanoTime() / 1000L; |
||||
|
// presentationTimeUs should be monotonic
|
||||
|
// otherwise muxer fail to write
|
||||
|
if (result < prevOutputPTSUs) |
||||
|
result = (prevOutputPTSUs - result) + result; |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,188 @@ |
|||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.media.MediaMuxer; |
||||
|
import android.os.Environment; |
||||
|
import android.text.TextUtils; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.io.File; |
||||
|
import java.io.IOException; |
||||
|
import java.nio.ByteBuffer; |
||||
|
import java.text.SimpleDateFormat; |
||||
|
import java.util.GregorianCalendar; |
||||
|
import java.util.Locale; |
||||
|
|
||||
|
public class MediaMuxerWrapper { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaMuxerWrapper"; |
||||
|
|
||||
|
private static final String DIR_NAME = "USBCameraTest"; |
||||
|
private static final SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); |
||||
|
|
||||
|
private String mOutputPath; |
||||
|
private final MediaMuxer mMediaMuxer; // API >= 18
|
||||
|
private int mEncoderCount, mStatredCount; |
||||
|
private boolean mIsStarted; |
||||
|
private MediaEncoder mVideoEncoder, mAudioEncoder; |
||||
|
|
||||
|
public MediaMuxerWrapper(String path) throws IOException { |
||||
|
try { |
||||
|
// 保存到自定义路径还是手机默认Movies路径
|
||||
|
if (TextUtils.isEmpty(path)) |
||||
|
mOutputPath = getCaptureFile(Environment.DIRECTORY_MOVIES, ".mp4").toString(); |
||||
|
mOutputPath = path; |
||||
|
|
||||
|
} catch (final NullPointerException e) { |
||||
|
throw new RuntimeException("This app has no permission of writing external storage"); |
||||
|
} |
||||
|
mMediaMuxer = new MediaMuxer(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4); |
||||
|
mEncoderCount = mStatredCount = 0; |
||||
|
mIsStarted = false; |
||||
|
} |
||||
|
|
||||
|
public String getOutputPath() { |
||||
|
return mOutputPath; |
||||
|
} |
||||
|
|
||||
|
public void prepare() throws IOException { |
||||
|
if (mVideoEncoder != null) |
||||
|
mVideoEncoder.prepare(); |
||||
|
if (mAudioEncoder != null) |
||||
|
mAudioEncoder.prepare(); |
||||
|
} |
||||
|
|
||||
|
public void startRecording() { |
||||
|
if (mVideoEncoder != null) |
||||
|
mVideoEncoder.startRecording(); |
||||
|
if (mAudioEncoder != null) |
||||
|
mAudioEncoder.startRecording(); |
||||
|
} |
||||
|
|
||||
|
public void stopRecording() { |
||||
|
if (mVideoEncoder != null) |
||||
|
mVideoEncoder.stopRecording(); |
||||
|
mVideoEncoder = null; |
||||
|
if (mAudioEncoder != null) |
||||
|
mAudioEncoder.stopRecording(); |
||||
|
mAudioEncoder = null; |
||||
|
} |
||||
|
|
||||
|
public synchronized boolean isStarted() { |
||||
|
return mIsStarted; |
||||
|
} |
||||
|
|
||||
|
//**********************************************************************
|
||||
|
//**********************************************************************
|
||||
|
/** |
||||
|
* assign encoder to this calss. this is called from encoder. |
||||
|
* @param encoder instance of MediaVideoEncoder or MediaAudioEncoder |
||||
|
*/ |
||||
|
/*package*/ void addEncoder(final MediaEncoder encoder) { |
||||
|
if (encoder instanceof MediaVideoEncoder) { |
||||
|
if (mVideoEncoder != null) |
||||
|
throw new IllegalArgumentException("Video encoder already added."); |
||||
|
mVideoEncoder = encoder; |
||||
|
} else if (encoder instanceof MediaSurfaceEncoder) { |
||||
|
if (mVideoEncoder != null) |
||||
|
throw new IllegalArgumentException("Video encoder already added."); |
||||
|
mVideoEncoder = encoder; |
||||
|
} else if (encoder instanceof MediaVideoBufferEncoder) { |
||||
|
if (mVideoEncoder != null) |
||||
|
throw new IllegalArgumentException("Video encoder already added."); |
||||
|
mVideoEncoder = encoder; |
||||
|
} else if (encoder instanceof MediaAudioEncoder) { |
||||
|
if (mAudioEncoder != null) |
||||
|
throw new IllegalArgumentException("Video encoder already added."); |
||||
|
mAudioEncoder = encoder; |
||||
|
} else |
||||
|
throw new IllegalArgumentException("unsupported encoder"); |
||||
|
mEncoderCount = (mVideoEncoder != null ? 1 : 0) + (mAudioEncoder != null ? 1 : 0); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* request start recording from encoder |
||||
|
* @return true when muxer is ready to write |
||||
|
*/ |
||||
|
/*package*/ synchronized boolean start() { |
||||
|
if (DEBUG) Log.v(TAG, "start:"); |
||||
|
mStatredCount++; |
||||
|
if ((mEncoderCount > 0) && (mStatredCount == mEncoderCount)) { |
||||
|
mMediaMuxer.start(); |
||||
|
mIsStarted = true; |
||||
|
notifyAll(); |
||||
|
if (DEBUG) Log.v(TAG, "MediaMuxer started:"); |
||||
|
} |
||||
|
return mIsStarted; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* request stop recording from encoder when encoder received EOS |
||||
|
*/ |
||||
|
/*package*/ synchronized void stop() { |
||||
|
if (DEBUG) Log.v(TAG, "stop:mStatredCount=" + mStatredCount); |
||||
|
mStatredCount--; |
||||
|
if ((mEncoderCount > 0) && (mStatredCount <= 0)) { |
||||
|
try { |
||||
|
mMediaMuxer.stop(); |
||||
|
} catch (final Exception e) { |
||||
|
Log.w(TAG, e); |
||||
|
} |
||||
|
mIsStarted = false; |
||||
|
if (DEBUG) Log.v(TAG, "MediaMuxer stopped:"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* assign encoder to muxer |
||||
|
* @param format |
||||
|
* @return minus value indicate error |
||||
|
*/ |
||||
|
/*package*/ synchronized int addTrack(final MediaFormat format) { |
||||
|
if (mIsStarted) |
||||
|
throw new IllegalStateException("muxer already started"); |
||||
|
final int trackIx = mMediaMuxer.addTrack(format); |
||||
|
if (DEBUG) Log.i(TAG, "addTrack:trackNum=" + mEncoderCount + ",trackIx=" + trackIx + ",format=" + format); |
||||
|
return trackIx; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* write encoded data to muxer |
||||
|
* @param trackIndex |
||||
|
* @param byteBuf |
||||
|
* @param bufferInfo |
||||
|
*/ |
||||
|
/*package*/ synchronized void writeSampleData(final int trackIndex, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo) { |
||||
|
if (mStatredCount > 0) |
||||
|
mMediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo); |
||||
|
} |
||||
|
|
||||
|
//**********************************************************************
|
||||
|
//**********************************************************************
|
||||
|
/** |
||||
|
* generate output file |
||||
|
* @param type Environment.DIRECTORY_MOVIES / Environment.DIRECTORY_DCIM etc. |
||||
|
* @param ext .mp4(.m4a for audio) or .png |
||||
|
* @return return null when this app has no writing permission to external storage. |
||||
|
*/ |
||||
|
public static final File getCaptureFile(final String type, final String ext) { |
||||
|
final File dir = new File(Environment.getExternalStoragePublicDirectory(type), DIR_NAME); |
||||
|
Log.d(TAG, "path=" + dir.toString()); |
||||
|
dir.mkdirs(); |
||||
|
if (dir.canWrite()) { |
||||
|
return new File(dir, getDateTimeString() + ext); |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* get current date and time as String |
||||
|
* @return |
||||
|
*/ |
||||
|
private static final String getDateTimeString() { |
||||
|
final GregorianCalendar now = new GregorianCalendar(); |
||||
|
return mDateTimeFormat.format(now.getTime()); |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,196 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaCodecInfo; |
||||
|
import android.media.MediaCodecList; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.util.Log; |
||||
|
import android.view.Surface; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
public class MediaSurfaceEncoder extends MediaEncoder implements IVideoEncoder { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaSurfaceEncoder"; |
||||
|
|
||||
|
private static final String MIME_TYPE = "video/avc"; |
||||
|
// parameters for recording
|
||||
|
private final int mWidth, mHeight; |
||||
|
private static final int FRAME_RATE = 15; |
||||
|
private static final float BPP = 0.50f; |
||||
|
|
||||
|
private Surface mSurface; |
||||
|
|
||||
|
public MediaSurfaceEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
||||
|
super(muxer, listener); |
||||
|
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
||||
|
mWidth = width; |
||||
|
mHeight = height; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Returns the encoder's input surface. |
||||
|
*/ |
||||
|
public Surface getInputSurface() { |
||||
|
return mSurface; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void prepare() throws IOException { |
||||
|
if (DEBUG) Log.i(TAG, "prepare: "); |
||||
|
mTrackIndex = -1; |
||||
|
mMuxerStarted = mIsEOS = false; |
||||
|
|
||||
|
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
||||
|
if (videoCodecInfo == null) { |
||||
|
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
||||
|
return; |
||||
|
} |
||||
|
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
||||
|
|
||||
|
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18
|
||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
||||
|
if (DEBUG) Log.i(TAG, "format: " + format); |
||||
|
|
||||
|
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
||||
|
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
||||
|
// get Surface for encoder input
|
||||
|
// this method only can call between #configure and #start
|
||||
|
mSurface = mMediaCodec.createInputSurface(); // API >= 18
|
||||
|
mMediaCodec.start(); |
||||
|
if (DEBUG) Log.i(TAG, "prepare finishing"); |
||||
|
if (mListener != null) { |
||||
|
try { |
||||
|
mListener.onPrepared(this); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "prepare:", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void release() { |
||||
|
if (DEBUG) Log.i(TAG, "release:"); |
||||
|
if (mSurface != null) { |
||||
|
mSurface.release(); |
||||
|
mSurface = null; |
||||
|
} |
||||
|
super.release(); |
||||
|
} |
||||
|
|
||||
|
private int calcBitRate() { |
||||
|
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
||||
|
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
||||
|
return bitrate; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select the first codec that match a specific MIME type |
||||
|
* @param mimeType |
||||
|
* @return null if no codec matched |
||||
|
*/ |
||||
|
protected static final MediaCodecInfo selectVideoCodec(final String mimeType) { |
||||
|
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
||||
|
|
||||
|
// get the list of available codecs
|
||||
|
final int numCodecs = MediaCodecList.getCodecCount(); |
||||
|
for (int i = 0; i < numCodecs; i++) { |
||||
|
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
||||
|
|
||||
|
if (!codecInfo.isEncoder()) { // skipp decoder
|
||||
|
continue; |
||||
|
} |
||||
|
// select first codec that match a specific MIME type and color format
|
||||
|
final String[] types = codecInfo.getSupportedTypes(); |
||||
|
for (int j = 0; j < types.length; j++) { |
||||
|
if (types[j].equalsIgnoreCase(mimeType)) { |
||||
|
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
||||
|
final int format = selectColorFormat(codecInfo, mimeType); |
||||
|
if (format > 0) { |
||||
|
return codecInfo; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select color format available on specific codec and we can use. |
||||
|
* @return 0 if no colorFormat is matched |
||||
|
*/ |
||||
|
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
||||
|
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
||||
|
int result = 0; |
||||
|
final MediaCodecInfo.CodecCapabilities caps; |
||||
|
try { |
||||
|
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
||||
|
caps = codecInfo.getCapabilitiesForType(mimeType); |
||||
|
} finally { |
||||
|
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
||||
|
} |
||||
|
int colorFormat; |
||||
|
for (int i = 0; i < caps.colorFormats.length; i++) { |
||||
|
colorFormat = caps.colorFormats[i]; |
||||
|
if (isRecognizedVideoFormat(colorFormat)) { |
||||
|
if (result == 0) |
||||
|
result = colorFormat; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (result == 0) |
||||
|
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* color formats that we can use in this class |
||||
|
*/ |
||||
|
protected static int[] recognizedFormats; |
||||
|
static { |
||||
|
recognizedFormats = new int[] { |
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
|
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
|
||||
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static final boolean isRecognizedVideoFormat(final int colorFormat) { |
||||
|
if (DEBUG) Log.i(TAG, "isRecognizedVideoFormat:colorFormat=" + colorFormat); |
||||
|
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
||||
|
for (int i = 0; i < n; i++) { |
||||
|
if (recognizedFormats[i] == colorFormat) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,193 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaCodecInfo; |
||||
|
import android.media.MediaCodecList; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.util.Log; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
import java.nio.ByteBuffer; |
||||
|
|
||||
|
/** |
||||
|
* This class receives video images as ByteBuffer(strongly recommend direct ByteBuffer) as NV21(YUV420SP) |
||||
|
* and encode them to h.264. |
||||
|
* If you use this directly with IFrameCallback, you should know UVCCamera and it backend native libraries |
||||
|
* never execute color space conversion. This means that color tone of resulted movie will be different |
||||
|
* from that you expected/can see on screen. |
||||
|
*/ |
||||
|
public class MediaVideoBufferEncoder extends MediaEncoder implements IVideoEncoder { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaVideoBufferEncoder"; |
||||
|
|
||||
|
private static final String MIME_TYPE = "video/avc"; |
||||
|
// parameters for recording
|
||||
|
private static final int FRAME_RATE = 15; |
||||
|
private static final float BPP = 0.50f; |
||||
|
|
||||
|
private final int mWidth, mHeight; |
||||
|
protected int mColorFormat; |
||||
|
|
||||
|
public MediaVideoBufferEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
||||
|
super(muxer, listener); |
||||
|
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
||||
|
mWidth = width; |
||||
|
mHeight = height; |
||||
|
} |
||||
|
|
||||
|
public void encode(final ByteBuffer buffer) { |
||||
|
// if (DEBUG) Log.v(TAG, "encode:buffer=" + buffer);
|
||||
|
synchronized (mSync) { |
||||
|
if (!mIsCapturing || mRequestStop) return; |
||||
|
} |
||||
|
encode(buffer, buffer.capacity(), getPTSUs()); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void prepare() throws IOException { |
||||
|
if (DEBUG) Log.i(TAG, "prepare: "); |
||||
|
mTrackIndex = -1; |
||||
|
mMuxerStarted = mIsEOS = false; |
||||
|
|
||||
|
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
||||
|
if (videoCodecInfo == null) { |
||||
|
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
||||
|
return; |
||||
|
} |
||||
|
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
||||
|
|
||||
|
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, mColorFormat); |
||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
||||
|
if (DEBUG) Log.i(TAG, "format: " + format); |
||||
|
|
||||
|
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
||||
|
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
||||
|
mMediaCodec.start(); |
||||
|
if (DEBUG) Log.i(TAG, "prepare finishing"); |
||||
|
if (mListener != null) { |
||||
|
try { |
||||
|
mListener.onPrepared(this); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "prepare:", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
private int calcBitRate() { |
||||
|
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
||||
|
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
||||
|
return bitrate; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select the first codec that match a specific MIME type |
||||
|
* @param mimeType |
||||
|
* @return null if no codec matched |
||||
|
*/ |
||||
|
@SuppressWarnings("deprecation") |
||||
|
protected final MediaCodecInfo selectVideoCodec(final String mimeType) { |
||||
|
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
||||
|
|
||||
|
// get the list of available codecs
|
||||
|
final int numCodecs = MediaCodecList.getCodecCount(); |
||||
|
for (int i = 0; i < numCodecs; i++) { |
||||
|
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
||||
|
|
||||
|
if (!codecInfo.isEncoder()) { // skipp decoder
|
||||
|
continue; |
||||
|
} |
||||
|
// select first codec that match a specific MIME type and color format
|
||||
|
final String[] types = codecInfo.getSupportedTypes(); |
||||
|
for (int j = 0; j < types.length; j++) { |
||||
|
if (types[j].equalsIgnoreCase(mimeType)) { |
||||
|
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
||||
|
final int format = selectColorFormat(codecInfo, mimeType); |
||||
|
if (format > 0) { |
||||
|
mColorFormat = format; |
||||
|
return codecInfo; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select color format available on specific codec and we can use. |
||||
|
* @return 0 if no colorFormat is matched |
||||
|
*/ |
||||
|
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
||||
|
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
||||
|
int result = 0; |
||||
|
final MediaCodecInfo.CodecCapabilities caps; |
||||
|
try { |
||||
|
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
||||
|
caps = codecInfo.getCapabilitiesForType(mimeType); |
||||
|
} finally { |
||||
|
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
||||
|
} |
||||
|
int colorFormat; |
||||
|
for (int i = 0; i < caps.colorFormats.length; i++) { |
||||
|
colorFormat = caps.colorFormats[i]; |
||||
|
if (isRecognizedViewoFormat(colorFormat)) { |
||||
|
if (result == 0) |
||||
|
result = colorFormat; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (result == 0) |
||||
|
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* color formats that we can use in this class |
||||
|
*/ |
||||
|
protected static int[] recognizedFormats; |
||||
|
static { |
||||
|
recognizedFormats = new int[] { |
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
||||
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar, |
||||
|
MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar, |
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface,
|
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static final boolean isRecognizedViewoFormat(final int colorFormat) { |
||||
|
if (DEBUG) Log.i(TAG, "isRecognizedViewoFormat:colorFormat=" + colorFormat); |
||||
|
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
||||
|
for (int i = 0; i < n; i++) { |
||||
|
if (recognizedFormats[i] == colorFormat) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
@ -0,0 +1,228 @@ |
|||||
|
/* |
||||
|
* UVCCamera |
||||
|
* library and sample to access to UVC web camera on non-rooted Android device |
||||
|
* |
||||
|
* Copyright (c) 2014-2017 saki t_saki@serenegiant.com |
||||
|
* |
||||
|
* Licensed under the Apache License, Version 2.0 (the "License"); |
||||
|
* you may not use this file except in compliance with the License. |
||||
|
* You may obtain a copy of the License at |
||||
|
* |
||||
|
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
* |
||||
|
* Unless required by applicable law or agreed to in writing, software |
||||
|
* distributed under the License is distributed on an "AS IS" BASIS, |
||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
||||
|
* See the License for the specific language governing permissions and |
||||
|
* limitations under the License. |
||||
|
* |
||||
|
* All files in the folder are under this Apache License, Version 2.0. |
||||
|
* Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder |
||||
|
* may have a different license, see the respective files. |
||||
|
*/ |
||||
|
|
||||
|
package com.serenegiant.usb.encoder; |
||||
|
|
||||
|
import android.media.MediaCodec; |
||||
|
import android.media.MediaCodecInfo; |
||||
|
import android.media.MediaCodecList; |
||||
|
import android.media.MediaFormat; |
||||
|
import android.util.Log; |
||||
|
import android.view.Surface; |
||||
|
|
||||
|
import com.serenegiant.glutils.EGLBase; |
||||
|
import com.serenegiant.glutils.RenderHandler; |
||||
|
|
||||
|
import java.io.IOException; |
||||
|
|
||||
|
/** |
||||
|
* Encode texture images as H.264 video |
||||
|
* using MediaCodec. |
||||
|
* This class render texture images into recording surface |
||||
|
* camera from MediaCodec encoder using Open GL|ES |
||||
|
*/ |
||||
|
public class MediaVideoEncoder extends MediaEncoder implements IVideoEncoder { |
||||
|
private static final boolean DEBUG = true; // TODO set false on release
|
||||
|
private static final String TAG = "MediaVideoEncoder"; |
||||
|
|
||||
|
private static final String MIME_TYPE = "video/avc"; |
||||
|
// parameters for recording
|
||||
|
private final int mWidth, mHeight; |
||||
|
private static final int FRAME_RATE = 15; |
||||
|
private static final float BPP = 0.50f; |
||||
|
|
||||
|
private RenderHandler mRenderHandler; |
||||
|
private Surface mSurface; |
||||
|
|
||||
|
public MediaVideoEncoder(final MediaMuxerWrapper muxer, final int width, final int height, final MediaEncoderListener listener) { |
||||
|
super(muxer, listener); |
||||
|
if (DEBUG) Log.i(TAG, "MediaVideoEncoder: "); |
||||
|
mRenderHandler = RenderHandler.createHandler(TAG); |
||||
|
mWidth = width; |
||||
|
mHeight = height; |
||||
|
} |
||||
|
|
||||
|
public boolean frameAvailableSoon(final float[] tex_matrix) { |
||||
|
boolean result; |
||||
|
if (result = super.frameAvailableSoon()) |
||||
|
mRenderHandler.draw(tex_matrix); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* This method does not work correctly on this class, |
||||
|
* use #frameAvailableSoon(final float[]) instead |
||||
|
* @return |
||||
|
*/ |
||||
|
@Override |
||||
|
public boolean frameAvailableSoon() { |
||||
|
boolean result; |
||||
|
if (result = super.frameAvailableSoon()) |
||||
|
mRenderHandler.draw(null); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void prepare() throws IOException { |
||||
|
if (DEBUG) Log.i(TAG, "prepare: "); |
||||
|
mTrackIndex = -1; |
||||
|
mMuxerStarted = mIsEOS = false; |
||||
|
|
||||
|
final MediaCodecInfo videoCodecInfo = selectVideoCodec(MIME_TYPE); |
||||
|
if (videoCodecInfo == null) { |
||||
|
Log.e(TAG, "Unable to find an appropriate codec for " + MIME_TYPE); |
||||
|
return; |
||||
|
} |
||||
|
if (DEBUG) Log.i(TAG, "selected codec: " + videoCodecInfo.getName()); |
||||
|
|
||||
|
final MediaFormat format = MediaFormat.createVideoFormat(MIME_TYPE, mWidth, mHeight); |
||||
|
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); // API >= 18
|
||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, calcBitRate()); |
||||
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, FRAME_RATE); |
||||
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 10); |
||||
|
if (DEBUG) Log.i(TAG, "format: " + format); |
||||
|
|
||||
|
mMediaCodec = MediaCodec.createEncoderByType(MIME_TYPE); |
||||
|
mMediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); |
||||
|
// get Surface for encoder input
|
||||
|
// this method only can call between #configure and #start
|
||||
|
mSurface = mMediaCodec.createInputSurface(); // API >= 18
|
||||
|
mMediaCodec.start(); |
||||
|
if (DEBUG) Log.i(TAG, "prepare finishing"); |
||||
|
if (mListener != null) { |
||||
|
try { |
||||
|
mListener.onPrepared(this); |
||||
|
} catch (final Exception e) { |
||||
|
Log.e(TAG, "prepare:", e); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
public void setEglContext(final EGLBase.IContext sharedContext, final int tex_id) { |
||||
|
mRenderHandler.setEglContext(sharedContext, tex_id, mSurface, true); |
||||
|
} |
||||
|
|
||||
|
@Override |
||||
|
protected void release() { |
||||
|
if (DEBUG) Log.i(TAG, "release:"); |
||||
|
if (mSurface != null) { |
||||
|
mSurface.release(); |
||||
|
mSurface = null; |
||||
|
} |
||||
|
if (mRenderHandler != null) { |
||||
|
mRenderHandler.release(); |
||||
|
mRenderHandler = null; |
||||
|
} |
||||
|
super.release(); |
||||
|
} |
||||
|
|
||||
|
private int calcBitRate() { |
||||
|
final int bitrate = (int)(BPP * FRAME_RATE * mWidth * mHeight); |
||||
|
Log.i(TAG, String.format("bitrate=%5.2f[Mbps]", bitrate / 1024f / 1024f)); |
||||
|
return bitrate; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select the first codec that match a specific MIME type |
||||
|
* @param mimeType |
||||
|
* @return null if no codec matched |
||||
|
*/ |
||||
|
protected static final MediaCodecInfo selectVideoCodec(final String mimeType) { |
||||
|
if (DEBUG) Log.v(TAG, "selectVideoCodec:"); |
||||
|
|
||||
|
// get the list of available codecs
|
||||
|
final int numCodecs = MediaCodecList.getCodecCount(); |
||||
|
for (int i = 0; i < numCodecs; i++) { |
||||
|
final MediaCodecInfo codecInfo = MediaCodecList.getCodecInfoAt(i); |
||||
|
|
||||
|
if (!codecInfo.isEncoder()) { // skipp decoder
|
||||
|
continue; |
||||
|
} |
||||
|
// select first codec that match a specific MIME type and color format
|
||||
|
final String[] types = codecInfo.getSupportedTypes(); |
||||
|
for (int j = 0; j < types.length; j++) { |
||||
|
if (types[j].equalsIgnoreCase(mimeType)) { |
||||
|
if (DEBUG) Log.i(TAG, "codec:" + codecInfo.getName() + ",MIME=" + types[j]); |
||||
|
final int format = selectColorFormat(codecInfo, mimeType); |
||||
|
if (format > 0) { |
||||
|
return codecInfo; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* select color format available on specific codec and we can use. |
||||
|
* @return 0 if no colorFormat is matched |
||||
|
*/ |
||||
|
protected static final int selectColorFormat(final MediaCodecInfo codecInfo, final String mimeType) { |
||||
|
if (DEBUG) Log.i(TAG, "selectColorFormat: "); |
||||
|
int result = 0; |
||||
|
final MediaCodecInfo.CodecCapabilities caps; |
||||
|
try { |
||||
|
Thread.currentThread().setPriority(Thread.MAX_PRIORITY); |
||||
|
caps = codecInfo.getCapabilitiesForType(mimeType); |
||||
|
} finally { |
||||
|
Thread.currentThread().setPriority(Thread.NORM_PRIORITY); |
||||
|
} |
||||
|
int colorFormat; |
||||
|
for (int i = 0; i < caps.colorFormats.length; i++) { |
||||
|
colorFormat = caps.colorFormats[i]; |
||||
|
if (isRecognizedVideoFormat(colorFormat)) { |
||||
|
if (result == 0) |
||||
|
result = colorFormat; |
||||
|
break; |
||||
|
} |
||||
|
} |
||||
|
if (result == 0) |
||||
|
Log.e(TAG, "couldn't find a good color format for " + codecInfo.getName() + " / " + mimeType); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* color formats that we can use in this class |
||||
|
*/ |
||||
|
protected static int[] recognizedFormats; |
||||
|
static { |
||||
|
recognizedFormats = new int[] { |
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420Planar,
|
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
|
||||
|
// MediaCodecInfo.CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
|
||||
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
private static final boolean isRecognizedVideoFormat(final int colorFormat) { |
||||
|
if (DEBUG) Log.i(TAG, "isRecognizedVideoFormat:colorFormat=" + colorFormat); |
||||
|
final int n = recognizedFormats != null ? recognizedFormats.length : 0; |
||||
|
for (int i = 0; i < n; i++) { |
||||
|
if (recognizedFormats[i] == colorFormat) { |
||||
|
return true; |
||||
|
} |
||||
|
} |
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
} |
Binary file not shown.
Loading…
Reference in new issue