【问题标题】:Camera2 video recording without preview on Android: mp4 output file not fully playable在 Android 上没有预览的 Camera2 视频录制:mp4 输出文件无法完全播放
【发布时间】:2016-10-12 13:25:14
【问题描述】:

我正在尝试从我的三星 Galaxy S6(支持 1920x1080 约 30 fps)上的后置摄像头(面向脸部的摄像头)录制视频。如果不需要,我不想使用任何表面进行预览,因为这只会在后台发生。

我似乎可以正常工作,但输出文件无法以实际正确的方式播放。在我的 Windows 10 PC 上,Windows Media Player 将显示第一帧然后播放音频,VLC 不会显示任何帧。在我的手机上,录制的文件可以播放,但不能完全播放。它将保持第一帧 5-8 秒,然后在最后,剩余时间变为 0,显示的总时间发生变化,然后实际视频帧开始播放。在我的 Mac (10.9.5) 上,Quicktime 不会显示视频(虽然没有错误),但 Google Picasa 可以完美播放。我想在我的电脑上试用 Picasa,看看它是否在那里工作,但我无法再下载 Google Picasa,因为它已经日落了。

我尝试安装我找到的适用于 Windows 的编解码器包,但没有解决任何问题。 MediaInfo v0.7.85 报告此文件:

一般的 全名:C:\...\1465655479915.mp4 格式:MPEG-4 格式配置文件:基础媒体/版本 2 编解码器 ID:mp42 (isom/mp42) 文件大小:32.2 MiB 持续时间:15s 744ms 总比特率:17.1 Mbps 编码日期:UTC 2016-06-11 14:31:50 标记日期:UTC 2016-06-11 14:31:50 com.android.version:6.0.1 视频 编号:1 格式:AVC 格式/信息:高级视频编解码器 格式配置文件:高@L4 格式设置,CABAC:是 格式设置,ReFrames : 1 帧 格式设置,GOP:M=1,N=30 编解码器 ID:avc1 编解码器 ID/信息:高级视频编码 持续时间:15s 627ms 比特率:16.2 Mbps 宽度:1 920 像素 高度:1 080 像素 显示纵横比:16:9 帧率模式:可变 帧率:0.000 (0/1000) fps 最低帧率:0.000 fps 最大帧率:30.540 fps 色彩空间:YUV 色度二次采样:4:2:0 位深:8位 扫描类型:逐行扫描 流大小:0.00 字节 (0%) 源流大小:31.7 MiB (98%) 标题:视频句柄 英语语言 编码日期:UTC 2016-06-11 14:31:50 标记日期:UTC 2016-06-11 14:31:50 mdhd_持续时间:15627 声音的 编号:2 格式:AAC 格式/信息:高级音频编解码器 格式配置文件:LC 编解码器 ID:40 持续时间:15s 744ms 比特率模式:恒定 比特率:256 Kbps 频道:2 个频道 通道位置:前:L R 采样率:48.0 KHz 帧率:46.875 fps (1024 spf) 压缩模式:有损 流大小:492 KiB (1%) 标题:声音手柄 英语语言 编码日期:UTC 2016-06-11 14:31:50 标记日期:UTC 2016-06-11 14:31:50

我用来创建它的代码是:

package invisiblevideorecorder;

import android.content.Context;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CameraMetadata;
import android.hardware.camera2.CaptureRequest;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Environment;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Log;
import android.view.Surface;

import java.io.File;
import java.io.IOException;
import java.util.Arrays;

/**
 * @author Mark
 * @since 6/10/2016
 */
public class InvisibleVideoRecorder {
    private static final String TAG = "InvisibleVideoRecorder";
    private final CameraCaptureSessionStateCallback cameraCaptureSessionStateCallback = new CameraCaptureSessionStateCallback();
    private final CameraDeviceStateCallback cameraDeviceStateCallback = new CameraDeviceStateCallback();
    private MediaRecorder mediaRecorder;
    private CameraManager cameraManager;
    private Context context;

    private CameraDevice cameraDevice;

    private HandlerThread handlerThread;
    private Handler handler;

    public InvisibleVideoRecorder(Context context) {
        this.context = context;
        handlerThread = new HandlerThread("camera");
        handlerThread.start();
        handler = new Handler(handlerThread.getLooper());

        try {
            mediaRecorder = new MediaRecorder();

            mediaRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
            mediaRecorder.setVideoSource(MediaRecorder.VideoSource.SURFACE);

            final String filename = context.getExternalFilesDir(Environment.DIRECTORY_MOVIES).getAbsolutePath() + File.separator + System.currentTimeMillis() + ".mp4";
            mediaRecorder.setOutputFile(filename);
            Log.d(TAG, "start: " + filename);

            // by using the profile, I don't think I need to do any of these manually:
//            mediaRecorder.setVideoEncodingBitRate(16000000);
//            mediaRecorder.setVideoFrameRate(30);
//            mediaRecorder.setCaptureRate(30);
//            mediaRecorder.setVideoSize(1920, 1080);
//            mediaRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
//            mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);

//            Log.d(TAG, "start: 1 " + CamcorderProfile.hasProfile(CameraMetadata.LENS_FACING_BACK, CamcorderProfile.QUALITY_1080P));
            // true
//            Log.d(TAG, "start: 2 " + CamcorderProfile.hasProfile(CameraMetadata.LENS_FACING_BACK, CamcorderProfile.QUALITY_HIGH_SPEED_1080P));
            // false
//            Log.d(TAG, "start: 3 " + CamcorderProfile.hasProfile(CameraMetadata.LENS_FACING_BACK, CamcorderProfile.QUALITY_HIGH));
            // true

            CamcorderProfile profile = CamcorderProfile.get(CameraMetadata.LENS_FACING_BACK, CamcorderProfile.QUALITY_1080P);
            Log.d(TAG, "start: profile " + ToString.inspect(profile));
//          start: 0 android.media.CamcorderProfile@114016694 {
//                audioBitRate: 256000
//                audioChannels: 2
//                audioCodec: 3
//                audioSampleRate: 48000
//                duration: 30
//                fileFormat: 2
//                quality: 6
//                videoBitRate: 17000000
//                videoCodec: 2
//                videoFrameHeight: 1080
//                videoFrameRate: 30
//                videoFrameWidth: 1920
//            }
            mediaRecorder.setOrientationHint(0);
            mediaRecorder.setProfile(profile);
            mediaRecorder.prepare();
        } catch (IOException e) {
            Log.d(TAG, "start: exception" + e.getMessage());
        }

    }

    public void start() {
        Log.d(TAG, "start: ");

        cameraManager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE);
        try {
            cameraManager.openCamera(String.valueOf(CameraMetadata.LENS_FACING_BACK), cameraDeviceStateCallback, handler);
        } catch (CameraAccessException | SecurityException e) {
            Log.d(TAG, "start: exception " + e.getMessage());
        }

    }

    public void stop() {
        Log.d(TAG, "stop: ");
        mediaRecorder.stop();
        mediaRecorder.reset();
        mediaRecorder.release();
        cameraDevice.close();
        try {
            handlerThread.join();
        } catch (InterruptedException e) {

        }
    }

    private class CameraCaptureSessionStateCallback extends CameraCaptureSession.StateCallback {
        private final static String TAG = "CamCaptSessionStCb";

        @Override
        public void onActive(CameraCaptureSession session) {
            Log.d(TAG, "onActive: ");
            super.onActive(session);
        }

        @Override
        public void onClosed(CameraCaptureSession session) {
            Log.d(TAG, "onClosed: ");
            super.onClosed(session);
        }

        @Override
        public void onConfigured(CameraCaptureSession session) {
            Log.d(TAG, "onConfigured: ");
        }

        @Override
        public void onConfigureFailed(CameraCaptureSession session) {
            Log.d(TAG, "onConfigureFailed: ");
        }

        @Override
        public void onReady(CameraCaptureSession session) {
            Log.d(TAG, "onReady: ");
            super.onReady(session);
            try {
                CaptureRequest.Builder builder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
                builder.addTarget(mediaRecorder.getSurface());
                CaptureRequest request = builder.build();
                session.setRepeatingRequest(request, null, handler);
                mediaRecorder.start();
            } catch (CameraAccessException e) {
                Log.d(TAG, "onConfigured: " + e.getMessage());

            }
        }

        @Override
        public void onSurfacePrepared(CameraCaptureSession session, Surface surface) {
            Log.d(TAG, "onSurfacePrepared: ");
            super.onSurfacePrepared(session, surface);
        }
    }

    private class CameraDeviceStateCallback extends CameraDevice.StateCallback {
        private final static String TAG = "CamDeviceStateCb";

        @Override
        public void onClosed(CameraDevice camera) {
            Log.d(TAG, "onClosed: ");
            super.onClosed(camera);
        }

        @Override
        public void onDisconnected(CameraDevice camera) {
            Log.d(TAG, "onDisconnected: ");
        }

        @Override
        public void onError(CameraDevice camera, int error) {
            Log.d(TAG, "onError: ");
        }

        @Override
        public void onOpened(CameraDevice camera) {
            Log.d(TAG, "onOpened: ");
            cameraDevice = camera;
            try {
                camera.createCaptureSession(Arrays.asList(mediaRecorder.getSurface()), cameraCaptureSessionStateCallback, handler);
            } catch (CameraAccessException e) {
                Log.d(TAG, "onOpened: " + e.getMessage());
            }
        }
    }

}

我遵循了 Android 源代码(测试和应用程序)代码,以及我在 github 上找到的几个示例,以便弄清楚这一点,因为 camera2 API 还没有很好的文档记录。

有什么明显的我做错了吗?或者,我只是在 Mac 上缺少编解码器以供 Quicktime 使用,而在我的 PC 上缺少编解码器以供 Windows Media Player 和 VLC 使用?我还没有尝试在 Linux 上播放这些文件,所以我还不知道那里会发生什么。哦,如果我将 mp4 文件上传到 photos.google.com,它们也可以在那里完全正确地播放。

谢谢! 标记

【问题讨论】:

  • 您是否有任何运气解决了这个问题,或者尝试使用我下面的答案?只是对未解决的旧问题做一些整理工作:) 谢谢。
  • @GrahamHarper 我一直在尝试处理这个确切的问题。令人沮丧的是它会影响这样一组特定的设备。有关决议的任何更新?它是我希望在运行时即时执行的操作,因此我可以使用您提供的解决方案

标签: video mp4 codec mediarecorder android-camera2


【解决方案1】:

我的团队在开发基于 Camera2 API 的插件时遇到了类似的问题,但它只影响了三星 Galaxy S7(我们也有一个 S6 用于测试,但没有出现这种行为)。

该问题似乎是由三星相机固件中的错误引起的,并且是在设备退出深度睡眠(Android 6.0 Marshmallow 中的超低功耗模式)时触发的。从深度睡眠中恢复后,使用 Camera2 MediaRecorder 捕获和编码的任何视频的第一帧都有非常长的帧持续时间 - 有时与视频本身的总持续时间一样长或更长。

因此,在播放时,第一帧会显示这么长的持续时间,而音频会继续播放。第一帧完成显示后,其余帧将正常播放。

我们发现其他人有类似问题discussing the issue on GitHub

问题是某些运行 Marshmallow 的设备出现深度睡眠问题。它似乎与 CPU 相关,因为 Verizon 上的 S7 没有问题,但 AT&T 上的 S7 确实有问题。我在升级到 Marshmallow 的 S6 Verizon 手机上看到了这一点。

为了进行复制,请在连接到 USB 时重新启动设备。运行示例。一切都应该没问题。然后,断开设备,让它进入深度睡眠(屏幕关闭,5 分钟内没有移动),然后重试。一旦设备进入深度睡眠,问题就会出现。

我们最终使用了cybaker's proposed workaround;也就是说,当创建视频文件时,检查视频第一帧的持续时间。如果看起来不正确,请使用合理的帧持续时间重新编码视频:

DataSource channel = new FileDataSourceImpl(rawFile);
IsoFile isoFile = new IsoFile(channel);

List<TrackBox> trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class);
boolean sampleError = false;
for (TrackBox trackBox : trackBoxes) {
    TimeToSampleBox.Entry firstEntry = trackBox.getMediaBox().getMediaInformationBox().getSampleTableBox().getTimeToSampleBox().getEntries().get(0);

    // Detect if first sample is a problem and fix it in isoFile
    // This is a hack. The audio deltas are 1024 for my files, and video deltas about 3000
    // 10000 seems sufficient since for 30 fps the normal delta is about 3000
    if(firstEntry.getDelta() > 10000) {
        sampleError = true;
        firstEntry.setDelta(3000);
    }
}

if(sampleError) {
    Movie movie = new Movie();
    for (TrackBox trackBox : trackBoxes) {
            movie.addTrack(new Mp4TrackImpl(channel.toString() + "[" + trackBox.getTrackHeaderBox().getTrackId() + "]" , trackBox));
    }
    movie.setMatrix(isoFile.getMovieBox().getMovieHeaderBox().getMatrix());
    Container out = new DefaultMp4Builder().build(movie);

    //delete file first!
    FileChannel fc = new RandomAccessFile(rawFile.getName(), "rw").getChannel();
    out.writeContainer(fc);
    fc.close();
    Log.d(TAG, "Finished correcting raw video");
}

希望这会为您指明正确的方向!

【讨论】:

  • 我得到了一个带有List&lt;TrackBox&gt; trackBoxes = isoFile.getMovieBox().getBoxes(TrackBox.class); 行的空指针。 getMovieBox() 方法是罪魁祸首。我将实际的 mp4 文件传递​​给 DataSource,这是正确的吗?
  • *无法解析符号 FileDataSourceImpl。 FileDataSourceImpl 在什么库?
  • @GrahamHarper 谢谢你,先生。您的解决方案完美运行,但对于 Android,Sannies/Mp4parser 库必须为(实现 'com.googlecode.mp4parser:isoparser:1.1.22')。新的库版本不起作用。我在 (implementation 'org.mp4parser:isoparser:1.9.27') 上测试过
【解决方案2】:

请注意,上面 harper 发布的代码需要这些依赖项:

compile 'com.googlecode.mp4parser:isoparser:1.1.22'

【讨论】:

    猜你喜欢
    • 2014-02-01
    • 2011-09-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-09
    • 2012-11-07
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多