Видео, снятое с помощью устройства записи экрана Android, не может воспроизводиться в веб-браузере

У меня возникает проблема, когда я пытаюсь воспроизвести видео в веб-браузере в компоненте, файл вообще не может воспроизводиться. Файл был захвачен на устройстве Android с помощью MediaRecorder и MediaProjection и попытался записать экран. Вот код, как я инициализирую 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 может), и вы обнаружите, что файл не может воспроизводиться. Но вы можете воспроизвести его на любом стороннем медиаплеере на ПК. Итак, в чем именно проблема, что файл не может быть воспроизведен в браузере?

Этот видеофайл изначально был создан из двух файлов (видеодорожки и звуковой дорожки). Я использую 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;
}

person miclendor    schedule 12.02.2019    source источник
comment
это не проблема, поскольку браузер не должен воспроизводить все возможные форматы. Либо вам нужно записать его в формате, поддерживаемом браузером, либо использовать видеоплеер для воспроизведения (не браузер)   -  person Vladyslav Matviienko    schedule 12.02.2019
comment
гм .. Я знаю, что браузер не может поддерживать все форматы, но MediaRecorder может обеспечивать только поддержку ограниченного формата, а mp4 - распространенный формат, который я считаю. Я нахожу, есть ли проблема в моем коде. Кроме того, игра в браузере - это функция, которая действительно нужна нашей платформе. Я также думаю о транскодировании видеофайла, что действительно сложно. В любом случае, спасибо за ответ.   -  person miclendor    schedule 12.02.2019


Ответы (1)


Если вы поместите обработчик '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 здесь).

2 байта - это немного для аудиосэмпла. Небольшое копание показывает, что на самом деле это копия "Audio Specific Config" для вашей звуковой дорожки, что странно, потому что эта информация уже присутствует в заголовках .mp4. Он дублируется в образец с отметкой времени 0 (первый образец); Не знаю почему.

Вы можете ознакомиться с документами для _3 _; вы не вызывали его, и в документации указано:

Если этот метод не вызывается, выходной файл не будет содержать звуковой дорожки.

Тем не менее, ваш файл содержит звуковую дорожку. Итак, это может потребовать дальнейшего расследования.

ИЗМЕНИТЬ

Учитывая это новое понимание вашей проблемы, кажется, что наиболее целесообразным решением будет просто принудительно удалить первый образец из вашего потока 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 мс. В этом случае, поскольку мы точно не знаем, почему ваш аудиокодер проявлял такое поведение, это можно интерпретировать либо как проблему с синхронизацией, либо как ее устранение.

person greeble31    schedule 14.02.2019
comment
Спасибо за такой профессиональный ответ. Я считаю, что есть проблема со звуковой дорожкой, как вы сказали. Я не вызывал setAudioEncoder (), потому что видеодорожка и звук разделены в двух файлах, так как они были захвачены в другом источнике. Когда я закончил запись, я использую mp4parser, чтобы объединить их в один файл. Теперь мне действительно интересно, что проблема в конфигурации комбинирования. - person miclendor; 18.02.2019
comment
Я не думаю, что mp4parser добавлено в тот первый образец. Вероятно, он уже присутствовал в вашем aacPath файле. Давайте посмотрим на код, который вы использовали для этой записи. - person greeble31; 18.02.2019
comment
Я уже отредактировал свой вопрос и обогатил свой код на экране записи. Спасибо за вашу заботу. - person miclendor; 19.02.2019
comment
Я думаю, вы могли неправильно понять. Я спрашиваю о коде, который вы используете для записи звука. - person greeble31; 19.02.2019
comment
Извини, это моя вина! Я не реализовал код для записи звука. Аудиофайл был получен из стороннего SDK, связанного с встречей. Я не могу получить доступ к деталям о том, как он записывает звук, поэтому я не могу предоставить это вам. - person miclendor; 20.02.2019
comment
О боже, это действительно помогло, хотя предстоит еще много тестов. Мой файл, по крайней мере, теперь можно воспроизводить в Chrome. Спасибо большое за вашу помощь. - person miclendor; 21.02.2019