Видео, снятое с устройства записи экрана Android, невозможно воспроизвести в веб-браузере - PullRequest
0 голосов
/ 12 февраля 2019

Возникает проблема, когда я пытаюсь воспроизвести видео в веб-браузере в компоненте, файл не воспроизводится вообще.Файл был захвачен на устройстве 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;
}

1 Ответ

0 голосов
/ 14 февраля 2019

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

...