【问题标题】:Video captured from Android screen recorder can not be played on web browser从 Android 屏幕录像机捕获的视频无法在 Web 浏览器上播放
【发布时间】:2023-04-09 02:47:01
【问题描述】:

当我尝试在组件中的网络浏览器上播放视频时遇到问题,该文件根本无法播放。该文件是使用 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;
}

【问题讨论】:

  • 这不是问题,因为浏览器不必播放所有可能的格式。要么你需要用浏览器支持的格式录制,要么用视频播放器播放(不是浏览器)
  • hum..我知道浏览器不能支持所有格式,但是MediaRecorder只能提供限制格式支持,我相信mp4是一种常见的格式。我正在查找我的代码中是否有任何问题。另外,在浏览器上播放是我们平台真正需要的功能。我也在考虑对非常复杂的视频文件进行转码。不管怎样,谢谢你的回答。

标签: android android-mediarecorder screen-recording android-mediaprojection


【解决方案1】:

如果您在 HTML5 视频元素上放置 'error' 处理程序,您会看到此文件产生以下错误 (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 字节对于音频样本来说有点小。稍微挖掘一下,这实际上是您的音轨的“音频特定配置”的副本,这很奇怪,因为该信息已经存在于 .mp4 标头中。它被复制到时间戳 0(第一个样本)的样本中;我不知道为什么。

您可能想查看setAudioEncoder() 的文档;您还没有调用它,并且文档状态:

如果不调用此方法,输出文件将不包含音轨。

但是,您的文件包含音轨。因此,这可能需要进一步调查。

编辑

鉴于对您的问题的这种新理解,似乎最方便的解决方案就是从您的 AAC 流中强制删除第一个样本。不妨用你的“组合”代码来做。我会像这样子类化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;
    }
};

我没有测试过这段代码。真的是一个非常老套的解决方案,它依赖于许多假设。它利用了 AAC 轨道中的所有样本恰好具有相同的“持续时间”这一事实;否则,您也必须使用类似的技术覆盖 getSampleDurations()

由于我们删除了一个样本但没有更改时间戳,这将使您的所有音频偏移约 23 毫秒。在这种情况下,由于我们并不确切知道为什么您的音频编码器首先会表现出这种行为,这可能被解释为导致计时问题或修复问题。

【讨论】:

  • 感谢您提供如此专业的回答。正如你所说,我相信音轨有问题。我没有调用 setAudioEncoder() ,因为视频轨道和音频在两个文件中是分开的,因为它们是在不同的源中捕获的。录制完成后,我使用 mp4parser 将它们组合成 1 个文件。现在我真的很想知道问题出在组合配置上。
  • 我认为mp4parser 没有添加到第一个示例中。它可能已经存在于您的aacPath 文件中。让我们看看你用来录制的代码。
  • 我已经在录制屏幕上编辑了我的问题并丰富了我的代码。感谢您的关心。
  • 我想你可能误解了。我问的是你用来录制音频的代码。
  • 对不起,我的错!我还没有实现录制音频的代码。音频文件来自与会议相关的第三方 SDK。我无法访问有关它如何录制音频的任何详细信息,因此无法为您提供。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-03-05
  • 1970-01-01
  • 2013-02-25
  • 2021-05-15
  • 1970-01-01
相关资源
最近更新 更多