从 Android 屏幕录像机捕获的视频无法在网络浏览器上播放
Video captured from Android screen recorder can not be played on web browser
我尝试在组件中的网络浏览器上播放视频时遇到问题,文件根本无法播放。该文件是使用 MediaRecorder 和 MediaProjection 在 Android 设备上捕获的,并尝试录制屏幕。
这是我如何初始化 MediaRecorder 的代码:
public class ScreenRecordService extends Service {
private static final String TAG = ScreenRecordService.class.getSimpleName();
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
private static final int DISPLAY_WIDTH = 960;
private static final int DISPLAY_HEIGHT = 540;
private float mDensity;
private int mRotation;
private boolean mIsRecording;
private MediaProjectionManager mProjectionManager;
private MediaProjection mMediaProjection;
private VirtualDisplay mVirtualDisplay;
private MediaProjectionCallback mMediaProjectionCallback;
private MediaRecorder mMediaRecorder;
private String mFilePath;
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
private class MediaProjectionCallback extends MediaProjection.Callback {
@Override
public void onStop() {
try {
if (mIsRecording) {
mIsRecording = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
}
mMediaProjection = null;
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_SUCCESS));
} catch (Exception e) {
e.printStackTrace();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_FAIL));
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onStopCall(EventRecorder.Client clientEvent) {
if (clientEvent.messageType == EventRecorder.CLIENT_STOP_RECORD) {
stopRecording();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
HermesEventBus.getDefault().register(this);
AppManager.getInstance().addService(this);
mProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
mMediaProjectionCallback = new MediaProjectionCallback();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (mProjectionManager == null) {
mProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
}
if (intent != null) {
mDensity = intent.getFloatExtra("density", 0f);
mRotation = intent.getIntExtra("rotation", 0);
mFilePath = intent.getStringExtra(Const.Intent.INFO);
JLog.d(TAG, mFilePath);
startRecording(intent);
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
AppManager.getInstance().removeService(this);
}
private void startRecording(Intent intent) {
try {
if (!mIsRecording) {
mMediaProjection = mProjectionManager.getMediaProjection(RESULT_OK, intent);
mMediaProjection.registerCallback(mMediaProjectionCallback, null);
initRecorder();
mVirtualDisplay = createVirtualDisplay();
mMediaRecorder.start();
mIsRecording = true;
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_START_SUCCESS));
}
} catch (Exception e) {
e.printStackTrace();
mIsRecording = false;
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_START_FAIL));
}
}
private void stopRecording() {
try {
if (mIsRecording) {
mMediaRecorder.stop();
mMediaRecorder.reset();
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_SUCCESS));
}
} catch (Exception e) {
e.printStackTrace();
mIsRecording = false;
if (mMediaRecorder != null) {
mMediaRecorder.reset();
}
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_FAIL));
}
}
private VirtualDisplay createVirtualDisplay() {
return mMediaProjection.createVirtualDisplay(getString(R.string.video_record), DISPLAY_WIDTH, DISPLAY_HEIGHT, (int) mDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
}
private void stopScreenSharing() {
if (mVirtualDisplay == null) {
return;
}
mVirtualDisplay.release();
destroyMediaProjection();
mIsRecording = false;
}
private void initRecorder() {
int bitRateQuality = PrefsUtils.getInstance(this, Const.Pref.FILE_COMMON).getInt(Const.Pref.KEY_RECORD_BITRATE, Const.Setting.QUALITY_MID);
int bitRate;
if (bitRateQuality == Const.Setting.QUALITY_HIGH) {
bitRate = 1536000;
} else if (bitRateQuality == Const.Setting.QUALITY_MID) {
bitRate = 1024 * 1024;
} else {
bitRate = 512000;
}
if (mMediaRecorder == null) {
mMediaRecorder = new MediaRecorder();
}
try {
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //THREE_GPP
mMediaRecorder.setOutputFile(mFilePath);
mMediaRecorder.setVideoSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setVideoFrameRate(8); // 30
mMediaRecorder.setVideoEncodingBitRate(bitRate);
int orientation = ORIENTATIONS.get(mRotation + 90);
mMediaRecorder.setOrientationHint(orientation);
mMediaRecorder.prepare();
mMediaRecorder.setOnInfoListener((mr, what, extra) -> {
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
stopRecording();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void destroyMediaProjection() {
if (mMediaProjection != null) {
mMediaProjection.unregisterCallback(mMediaProjectionCallback);
mMediaProjection.stop();
mMediaProjection = null;
}
JLog.i(TAG, "MediaProjection Stopped");
}
}
这是我上传的文件。
http://eachdoctorvideotest.oss-cn-shenzhen.aliyuncs.com/1103/videoRecord/input/ali_TVM1103vRecordIn20190212154904841.mp4
把url粘贴到任何浏览器(我用Chrome和chrome不能播放,但是Safari可以),你会发现文件不能播放。但您可以在 PC 上的任何第三方媒体播放器上播放它。那么,文件无法在浏览器上播放的具体问题是什么?
此视频文件最初由两个文件(视频轨道和音频轨道)生成。
我使用 mp4parser 合并曲目,您可以在此处查看库:
https://github.com/sannies/mp4parser
这是我用来组合它们的关键代码:
public boolean muxAacMp4(String mp4Path, String aacPath, String outPath) {
boolean flag = false;
try {
AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(aacPath));
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);
resultMovie.addTrack(aacTrack);
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
flag = true;
Log.e("update_tag", "merge finish");
} catch (Exception e) {
e.printStackTrace();
flag = false;
}
return flag;
}
如果您将 'error'
处理程序放在 HTML5 视频元素上,您会看到此文件产生以下错误 (Chrome 71):
Error 3; details: PIPELINE_ERROR_DECODE: Failed to send audio packet for decoding: timestamp=0 duration=32000 size=2 side_data_size=0 is_key_frame=1 encrypted=0 discard_padding (us)=(0, 0)
(仅供参考:在 github here 上讨论了类似的错误)。
2 个字节对于音频样本来说有点小。稍加挖掘就会发现,这实际上是您音轨的 "Audio Specific Config" 的副本,这很奇怪,因为该信息已经存在于 .mp4 headers 中。它被复制到时间戳为 0 的样本中(第一个样本);我不确定为什么。
您可能需要查看 setAudioEncoder()
的文档;你还没有调用它,文档状态:
If this method is not called, the output file will not contain an audio track.
但是,您的文件包含音轨。因此,这可能需要进一步调查。
编辑
鉴于对您的问题有了新的认识,最权宜之计的解决方案似乎就是从您的 AAC 流中强行删除第一个样本。不妨使用您的 "combining" 代码来完成。我会像这样子类化 AACTrackImpl
:
AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(aacPath)) {
boolean mAltered = false;
@Override
List<Sample> getSamples() {
List<Samples> samples = super.getSamples();
if(!mAltered)
{
samples.remove(0);
mAltered=true;
}
return samples;
}
};
我还没有测试过这段代码。确实是一个 hack-y 解决方案,它依赖于许多假设。它利用了这样一个事实,即您的 AAC 轨道中的所有样本恰好具有相同的 "duration";否则,您也必须使用类似的技术覆盖 getSampleDurations()
。
由于我们删除了一个样本但没有更改时间戳,这将使您的所有音频偏移约 23 毫秒。在这种情况下,由于我们不完全知道为什么您的音频编码器首先表现出这种行为,这可能被解释为导致时序问题或修复问题。
我尝试在组件中的网络浏览器上播放视频时遇到问题,文件根本无法播放。该文件是使用 MediaRecorder 和 MediaProjection 在 Android 设备上捕获的,并尝试录制屏幕。 这是我如何初始化 MediaRecorder 的代码:
public class ScreenRecordService extends Service {
private static final String TAG = ScreenRecordService.class.getSimpleName();
private static final SparseIntArray ORIENTATIONS = new SparseIntArray();
private static final int DISPLAY_WIDTH = 960;
private static final int DISPLAY_HEIGHT = 540;
private float mDensity;
private int mRotation;
private boolean mIsRecording;
private MediaProjectionManager mProjectionManager;
private MediaProjection mMediaProjection;
private VirtualDisplay mVirtualDisplay;
private MediaProjectionCallback mMediaProjectionCallback;
private MediaRecorder mMediaRecorder;
private String mFilePath;
static {
ORIENTATIONS.append(Surface.ROTATION_0, 90);
ORIENTATIONS.append(Surface.ROTATION_90, 0);
ORIENTATIONS.append(Surface.ROTATION_180, 270);
ORIENTATIONS.append(Surface.ROTATION_270, 180);
}
private class MediaProjectionCallback extends MediaProjection.Callback {
@Override
public void onStop() {
try {
if (mIsRecording) {
mIsRecording = false;
mMediaRecorder.stop();
mMediaRecorder.reset();
}
mMediaProjection = null;
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_SUCCESS));
} catch (Exception e) {
e.printStackTrace();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_FAIL));
}
}
}
@Subscribe(threadMode = ThreadMode.MAIN)
public void onStopCall(EventRecorder.Client clientEvent) {
if (clientEvent.messageType == EventRecorder.CLIENT_STOP_RECORD) {
stopRecording();
}
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
@Override
public void onCreate() {
super.onCreate();
HermesEventBus.getDefault().register(this);
AppManager.getInstance().addService(this);
mProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
mMediaProjectionCallback = new MediaProjectionCallback();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
if (mProjectionManager == null) {
mProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE);
}
if (intent != null) {
mDensity = intent.getFloatExtra("density", 0f);
mRotation = intent.getIntExtra("rotation", 0);
mFilePath = intent.getStringExtra(Const.Intent.INFO);
JLog.d(TAG, mFilePath);
startRecording(intent);
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
super.onDestroy();
AppManager.getInstance().removeService(this);
}
private void startRecording(Intent intent) {
try {
if (!mIsRecording) {
mMediaProjection = mProjectionManager.getMediaProjection(RESULT_OK, intent);
mMediaProjection.registerCallback(mMediaProjectionCallback, null);
initRecorder();
mVirtualDisplay = createVirtualDisplay();
mMediaRecorder.start();
mIsRecording = true;
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_START_SUCCESS));
}
} catch (Exception e) {
e.printStackTrace();
mIsRecording = false;
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_START_FAIL));
}
}
private void stopRecording() {
try {
if (mIsRecording) {
mMediaRecorder.stop();
mMediaRecorder.reset();
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_SUCCESS));
}
} catch (Exception e) {
e.printStackTrace();
mIsRecording = false;
if (mMediaRecorder != null) {
mMediaRecorder.reset();
}
stopScreenSharing();
HermesEventBus.getDefault().post(new EventRecorder.Server(EventRecorder.SERVER_STOP_FAIL));
}
}
private VirtualDisplay createVirtualDisplay() {
return mMediaProjection.createVirtualDisplay(getString(R.string.video_record), DISPLAY_WIDTH, DISPLAY_HEIGHT, (int) mDensity,
DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mMediaRecorder.getSurface(), null, null);
}
private void stopScreenSharing() {
if (mVirtualDisplay == null) {
return;
}
mVirtualDisplay.release();
destroyMediaProjection();
mIsRecording = false;
}
private void initRecorder() {
int bitRateQuality = PrefsUtils.getInstance(this, Const.Pref.FILE_COMMON).getInt(Const.Pref.KEY_RECORD_BITRATE, Const.Setting.QUALITY_MID);
int bitRate;
if (bitRateQuality == Const.Setting.QUALITY_HIGH) {
bitRate = 1536000;
} else if (bitRateQuality == Const.Setting.QUALITY_MID) {
bitRate = 1024 * 1024;
} else {
bitRate = 512000;
}
if (mMediaRecorder == null) {
mMediaRecorder = new MediaRecorder();
}
try {
mMediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);
mMediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4); //THREE_GPP
mMediaRecorder.setOutputFile(mFilePath);
mMediaRecorder.setVideoSize(DISPLAY_WIDTH, DISPLAY_HEIGHT);
mMediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.H264);
mMediaRecorder.setVideoFrameRate(8); // 30
mMediaRecorder.setVideoEncodingBitRate(bitRate);
int orientation = ORIENTATIONS.get(mRotation + 90);
mMediaRecorder.setOrientationHint(orientation);
mMediaRecorder.prepare();
mMediaRecorder.setOnInfoListener((mr, what, extra) -> {
if (what == MEDIA_RECORDER_INFO_MAX_FILESIZE_REACHED) {
stopRecording();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
private void destroyMediaProjection() {
if (mMediaProjection != null) {
mMediaProjection.unregisterCallback(mMediaProjectionCallback);
mMediaProjection.stop();
mMediaProjection = null;
}
JLog.i(TAG, "MediaProjection Stopped");
}
}
这是我上传的文件。
http://eachdoctorvideotest.oss-cn-shenzhen.aliyuncs.com/1103/videoRecord/input/ali_TVM1103vRecordIn20190212154904841.mp4
把url粘贴到任何浏览器(我用Chrome和chrome不能播放,但是Safari可以),你会发现文件不能播放。但您可以在 PC 上的任何第三方媒体播放器上播放它。那么,文件无法在浏览器上播放的具体问题是什么?
此视频文件最初由两个文件(视频轨道和音频轨道)生成。 我使用 mp4parser 合并曲目,您可以在此处查看库:
https://github.com/sannies/mp4parser
这是我用来组合它们的关键代码:
public boolean muxAacMp4(String mp4Path, String aacPath, String outPath) {
boolean flag = false;
try {
AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(aacPath));
Movie videoMovie = MovieCreator.build(mp4Path);
Track videoTracks = null;
for (Track videoMovieTrack : videoMovie.getTracks()) {
if ("vide".equals(videoMovieTrack.getHandler())) {
videoTracks = videoMovieTrack;
}
}
Movie resultMovie = new Movie();
resultMovie.addTrack(videoTracks);
resultMovie.addTrack(aacTrack);
Container out = new DefaultMp4Builder().build(resultMovie);
FileOutputStream fos = new FileOutputStream(new File(outPath));
out.writeContainer(fos.getChannel());
fos.close();
flag = true;
Log.e("update_tag", "merge finish");
} catch (Exception e) {
e.printStackTrace();
flag = false;
}
return flag;
}
如果您将 'error'
处理程序放在 HTML5 视频元素上,您会看到此文件产生以下错误 (Chrome 71):
Error 3; details: PIPELINE_ERROR_DECODE: Failed to send audio packet for decoding: timestamp=0 duration=32000 size=2 side_data_size=0 is_key_frame=1 encrypted=0 discard_padding (us)=(0, 0)
(仅供参考:在 github here 上讨论了类似的错误)。
2 个字节对于音频样本来说有点小。稍加挖掘就会发现,这实际上是您音轨的 "Audio Specific Config" 的副本,这很奇怪,因为该信息已经存在于 .mp4 headers 中。它被复制到时间戳为 0 的样本中(第一个样本);我不确定为什么。
您可能需要查看 setAudioEncoder()
的文档;你还没有调用它,文档状态:
If this method is not called, the output file will not contain an audio track.
但是,您的文件包含音轨。因此,这可能需要进一步调查。
编辑
鉴于对您的问题有了新的认识,最权宜之计的解决方案似乎就是从您的 AAC 流中强行删除第一个样本。不妨使用您的 "combining" 代码来完成。我会像这样子类化 AACTrackImpl
:
AACTrackImpl aacTrack = new AACTrackImpl(new FileDataSourceImpl(aacPath)) {
boolean mAltered = false;
@Override
List<Sample> getSamples() {
List<Samples> samples = super.getSamples();
if(!mAltered)
{
samples.remove(0);
mAltered=true;
}
return samples;
}
};
我还没有测试过这段代码。确实是一个 hack-y 解决方案,它依赖于许多假设。它利用了这样一个事实,即您的 AAC 轨道中的所有样本恰好具有相同的 "duration";否则,您也必须使用类似的技术覆盖 getSampleDurations()
。
由于我们删除了一个样本但没有更改时间戳,这将使您的所有音频偏移约 23 毫秒。在这种情况下,由于我们不完全知道为什么您的音频编码器首先表现出这种行为,这可能被解释为导致时序问题或修复问题。