.setPreviewFpsRange():在相机 .setParameters() 中每秒更新帧时出现问题

.setPreviewFpsRange(): Having problems updating frames per second in camera .setParameters()

2015 年 10 月 28 日更新以反映当前进展。 我有一个应用程序,允许用户为 Motion JPEG 记录设置相机参数,创建 MJPEG 文件,然后用户可以修改这些设置并使用更新的设置创建另一个文件。当初始值不是 30 FPS 时,我在更新每秒帧数设置时遇到问题。当初始值为 30 FPS 时,我可以更新到不同的 FPS 级别并在该级别成功录制视频。但是,我无法从不等于 30FPS 的级别更新到另一个 FPM 级别。 LogCat 我崩溃了,显示

有问题

camera.setParameters(参数);

全部 LogCat 错误如下,

10-26 20:27:36.414: E/AndroidRuntime(2275): FATAL EXCEPTION: main
10-26 20:27:36.414: E/AndroidRuntime(2275): java.lang.RuntimeException: setParameters failed
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.hardware.Camera.native_setParameters(Native Method)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.hardware.Camera.setParameters(Camera.java:1333)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at net.blepsias.riverwatch.RiverWatch.setCamera(RiverWatch.java:191)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at net.blepsias.riverwatch.RiverWatch.onClick(RiverWatch.java:167)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.view.View.performClick(View.java:3514)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.view.View$PerformClick.run(View.java:14111)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.os.Handler.handleCallback(Handler.java:605)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.os.Handler.dispatchMessage(Handler.java:92)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.os.Looper.loop(Looper.java:137)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at android.app.ActivityThread.main(ActivityThread.java:4429)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at java.lang.reflect.Method.invokeNative(Native Method)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at java.lang.reflect.Method.invoke(Method.java:511)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:784)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:551)
10-26 20:27:36.414: E/AndroidRuntime(2275):     at dalvik.system.NativeStart.main(Native Method)

检查 LogCat 引用的第 5 行和第 6 行,这些对应于:

(191) camera.setParameters(parameters);
(167) setCamera(camera);

下面是应用程序。我还将包括布局 .xml 文件以供参考,以及用于接地的屏幕截图。

RiverWatch.java

public class RiverWatch extends Activity implements OnClickListener, SurfaceHolder.Callback, Camera.PreviewCallback {
public static final String LOGTAG = "VIDEOCAPTURE";

String szBoundaryStart = "\r\n\r\n--myboundary\r\nContent-Type: image/jpeg\r\nContent-Length: ";
String szBoundaryDeltaTime = "\r\nDelta-time: 110";
String szBoundaryEnd = "\r\n\r\n";

private SurfaceHolder holder;
private Camera camera;  
private CamcorderProfile camcorderProfile;

Spinner spinnerCamcorderProfile;
public TextView tvFramesPerSecond, tvJpegQuality, tvSegmentDuration;

boolean bRecording = false;
boolean bPreviewRunning = false;

int intFramesPerSecond = 30000; //this is 30fps...mult by 1,000
int intJpegQuality=50; //must be above 20
int intSegmentDuration=10;
boolean ckbxRepeat=false;

byte[] previewCallbackBuffer;

File mjpegFile;
FileOutputStream fos;
BufferedOutputStream bos;
Button btnStartRecord, btnStopRecord, btnExit, btnChange;

Camera.Parameters parameters;

@Override
public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    Date T = new Date();
    SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
    String szFileName = "videocapture-"+sdf.format(T)+"-";

    try {       
        mjpegFile = File.createTempFile(szFileName, ".mjpeg", Environment.getExternalStorageDirectory());               
    } catch (Exception e) {
        finish();
    }

    requestWindowFeature(Window.FEATURE_NO_TITLE);
    getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
            WindowManager.LayoutParams.FLAG_FULLSCREEN);
    setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
    setContentView(R.layout.main);

    tvFramesPerSecond = (TextView) this.findViewById(R.id.textboxframespersecondxml);
    int iFPS = intFramesPerSecond/1000;
    String szFPS = Integer.toString(iFPS);
    tvFramesPerSecond.setClickable(true);       
    tvFramesPerSecond.setText(szFPS);
    tvFramesPerSecond.setOnClickListener(new OnClickListener() {
        @Override
        public void onClick(View v) {
            getSupportedPreviewFpsRange();
        }
    });

    tvJpegQuality = (TextView) this.findViewById(R.id.textboxJpegQualityxml);
    String szJpegQuality = Integer.toString(intJpegQuality);
    tvJpegQuality.setText(szJpegQuality);

    tvSegmentDuration = (TextView) this.findViewById(R.id.textboxSegmentDurationxml);
    String szSegmentDuration = Integer.toString(intSegmentDuration);
    tvSegmentDuration.setText(szSegmentDuration);

    btnStartRecord = (Button) this.findViewById(R.id.StartRecordButton);
    btnStartRecord.setOnClickListener(this);

    btnStopRecord = (Button) this.findViewById(R.id.StopRecordButton);
    btnStopRecord.setOnClickListener(this);

    btnExit = (Button) this.findViewById(R.id.ExitButton);
    btnExit.setOnClickListener(this);

    btnChange = (Button) this.findViewById(R.id.ChangeButton);
    btnChange.setOnClickListener(this);

    camcorderProfile = CamcorderProfile.get(CamcorderProfile.QUALITY_TIME_LAPSE_480P);
    SurfaceView cameraView = (SurfaceView) findViewById(R.id.CameraView);
    holder = cameraView.getHolder();
    holder.addCallback(this);
    holder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);

    cameraView.setClickable(true);
    cameraView.setOnClickListener(this);
}

@Override
public void onClick(View v) {
        switch (v.getId()) {
        case R.id.StartRecordButton:
            try {
                fos = new FileOutputStream(mjpegFile);
                bos = new BufferedOutputStream(fos);
                bRecording = true;
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            Toast.makeText(this, "Recording started.", Toast.LENGTH_SHORT).show();
            break;
        case R.id.StopRecordButton:     
            try {
                bos.flush();
                bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            Toast.makeText(this, "Recording stopped.", Toast.LENGTH_SHORT).show();
            break;      

        case R.id.ChangeButton: 
            //Frames Per Second- expressed x1000 in the function                
            String szFPS=tvFramesPerSecond.getText().toString();
            int iFPS = Integer.parseInt(szFPS);
            intFramesPerSecond = iFPS *1000;

            //Jpeg quality- cant be <20 or >100, checks this and populates field with entered or corrected value.
            String szJpegQuality=tvJpegQuality.getText().toString();
            int intJpegQualityTemp = Integer.parseInt(szJpegQuality);
            if (intJpegQualityTemp < 21){//...can't be less than 21
                intJpegQuality = 21;
            }else if(intJpegQualityTemp > 100){//can't be greater than 100
                 intJpegQuality = 100;
            }else{ //quality is between 21 and 100...
                intJpegQuality = intJpegQualityTemp;
            }
            szJpegQuality = Integer.toString(intJpegQuality);
            tvJpegQuality.setText(szJpegQuality);   

            //Segment duration
            String szSegmentDuration=tvSegmentDuration.getText().toString();
            intSegmentDuration = Integer.parseInt(szSegmentDuration);

            releaseCamera();
            setCamera(camera);      

            camera.startPreview();  
            Toast.makeText(this, "Change button pressed.", Toast.LENGTH_SHORT).show();
            break;

        case R.id.ExitButton:
            System.exit(0);
            break;
        }
    }

public void releaseCamera(){
    camera.stopPreview();
   //camera.release();  //...cause crash
   //camera = null;
}
public void setCamera(Camera camera){
    Camera.Parameters parameters=camera.getParameters();
    parameters.setPreviewFpsRange(intFramesPerSecond, intFramesPerSecond);//note: This is fps x 1000 (!)
    parameters.setPreviewSize(camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
    Log.v(LOGTAG,"FPS: " + parameters.getSupportedPreviewFpsRange());
    camera.setParameters(parameters);
}


public void getSupportedPreviewFpsRange(){
/****************************************************************
 * getSupportedPreviewFpsRange()- Returns specified frame rate 
 * (.getSupportedPreviewFpsRange()) to log file and also displays 
 * as toast message.
 ****************************************************************/              
Camera.Parameters camParameter = camera.getParameters();
List<int[]> frame = camParameter.getSupportedPreviewFpsRange();
    Iterator<int[]> supportedPreviewFpsIterator = frame.iterator();
    while (supportedPreviewFpsIterator.hasNext()) {
        int[] tmpRate = supportedPreviewFpsIterator.next();
        StringBuffer sb = new StringBuffer();
        sb.append("SupportedPreviewRate: ");
        for (int i = tmpRate.length, j = 0; j < i; j++) {
            sb.append(tmpRate[j] + ", ");
        }
        Log.d(LOGTAG, "FPS6: " + sb.toString());
        Toast.makeText(this, "FPS = "+sb.toString(), Toast.LENGTH_SHORT).show();
    }//*****************end getSupportedPreviewFpsRange()**********************                                                 
}   

public void surfaceCreated(SurfaceHolder holder) {
    camera = Camera.open();
}
@SuppressLint("NewApi")
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    if (!bRecording) {
        if (bPreviewRunning = true){
            camera.stopPreview();
        } try {
            parameters = camera.getParameters();
            parameters.setPreviewSize(camcorderProfile.videoFrameWidth, camcorderProfile.videoFrameHeight);
            parameters.setPreviewFpsRange(intFramesPerSecond, intFramesPerSecond);//note: This is fps x 1000 (!)
            //p.setPreviewFrameRate(intFramesPerSecond);
            camera.setParameters(parameters);
            camera.setPreviewDisplay(holder);               
            camera.setPreviewCallback(this);
            camera.setDisplayOrientation(90);
            camera.startPreview();
            bPreviewRunning = true;
        }
        catch (IOException e) {
            e.printStackTrace();
        }   
    }
}

public void surfaceDestroyed(SurfaceHolder holder) {
    if (bRecording) {
        bRecording = false;
        try {
            bos.flush();
            bos.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    bPreviewRunning = false;
    camera.release();
    finish();
}   

public void onPreviewFrame(byte[] b, Camera c) {
    if (bRecording) {
        // Assuming ImageFormat.NV21
        if (parameters.getPreviewFormat() == ImageFormat.NV21) {
            try {
                YuvImage im = new YuvImage(b, ImageFormat.NV21, parameters.getPreviewSize().width, parameters.getPreviewSize().height, null);
                Rect r = new Rect(0,0,parameters.getPreviewSize().width,parameters.getPreviewSize().height);
                ByteArrayOutputStream jpegByteArrayOutputStream = new ByteArrayOutputStream();
                im.compressToJpeg(r, intJpegQuality, jpegByteArrayOutputStream);//note: qual = 20 or less doesn't work.
                byte[] jpegByteArray = jpegByteArrayOutputStream.toByteArray();
                byte[] boundaryBytes = (szBoundaryStart + jpegByteArray.length + szBoundaryDeltaTime + szBoundaryEnd).getBytes();
                bos.write(boundaryBytes);                                       
                bos.write(jpegByteArray);
                bos.flush();
                //bos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            Log.v(LOGTAG,"NOT THE RIGHT FORMAT");
        }
    }
}
@Override
public void onConfigurationChanged(Configuration conf){
    super.onConfigurationChanged(conf); 
  } 
}

布局main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<LinearLayout
     android:orientation="horizontal"
     android:layout_width="wrap_content" 
     android:layout_height="wrap_content">
<Button
    android:id="@+id/StartRecordButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Start Recording" />    
<Button
    android:id="@+id/StopRecordButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Stop Recording" />   
  <Button
    android:id="@+id/ChangeButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="50dip"
    android:text="Reset settings" /> 
</LinearLayout>
<LinearLayout
     android:orientation="horizontal"
     android:layout_width="match_parent" 
     android:layout_height="wrap_content"
     android:gravity="right">
 <TextView
    style="@style/myStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Frames/second:" />
<EditText 
        android:id="@+id/textboxframespersecondxml"
        android:editable="true"
        style="@style/myStyle"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:gravity="right"
        android:text="0"
        android:layout_marginRight="10dip"/>
</LinearLayout>
<LinearLayout
     android:orientation="horizontal"
     android:layout_width="match_parent" 
     android:layout_height="wrap_content"
     android:gravity="right">
 <TextView
    style="@style/myStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="JPEG image quality:" />
<EditText 
        android:id="@+id/textboxJpegQualityxml"
        android:editable="true"
        style="@style/myStyle"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:gravity="right"
        android:text="0"
        android:layout_marginRight="10dip"/>
</LinearLayout>
<LinearLayout
android:orientation="horizontal"
     android:layout_width="fill_parent"
     android:layout_height="wrap_content">
<TextView
    style="@style/myStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginLeft="10dip"
    android:text="Camcorder profile: " />
<LinearLayout
     android:orientation="horizontal"
     android:layout_width="match_parent" 
     android:layout_height="wrap_content"
     android:gravity="right">
 <TextView
    style="@style/myStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Segment duration (file length):" />
<EditText 
        android:id="@+id/textboxSegmentDurationxml"
        android:editable="true"
        style="@style/myStyle"
        android:layout_width="wrap_content" 
        android:layout_height="wrap_content"
        android:gravity="right"
        android:text="0"
        android:layout_marginRight="10dip"/>
 <TextView
    style="@style/myStyle"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text=" minutes" />
</LinearLayout>
<LinearLayout
    android:orientation="horizontal"
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:gravity="right" >     
<CheckBox
    android:id="@+id/repeat"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Repeat" />
<Button
    android:id="@+id/ExitButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Exit Application" />
</LinearLayout>
<SurfaceView
    android:id="@+id/CameraView"
    android:layout_width="fill_parent"
    android:layout_height="fill_parent" />
</LinearLayout>

屏幕截图: 显示字段、按钮和表面配置

解决方案: 看来上述大部分不稳定行为可能与主要测试设备 Panasonic Toughpad JT-B1 有关。 运行

.getSupportedPreviewFpsRange(); 

在此设备上 returns 范围为 8,000-30,000 fps。但是,此范围内的许多值会导致崩溃,而此范围外的某些值似乎工作正常。测试 Samsung S4 Active 导致 none 这些不一致,返回范围 (4,000 - 30,000) 中的所有值都工作正常,并且没有超出此范围的测试值显示任何预期的功能。

相机API 不允许将预览 FPS 范围设置为任意值。您应该查询相机参数以获取支持范围列表,并且不保证任何其他组合都有效。

原则上,Camera.setParameters() 使用不受支持的值是 undefined behavior。当您尝试相同的输入时,不同的设备会出现故障或工作方式不同。

当然,您应该停止预览以更改相机参数,然后再重新启动预览。

除此之外,您可能可以使用变通方法来保留受支持的参数。要达到 2 fps 并切换到 10 fps,您无需更改相机设置。您的逻辑可以按时间戳过滤掉 onPreviewFrame() 中的相关帧。


此外,您的代码在预览回调方面不够理想。首先,你应该在一个单独的处理线程上打开相机,然后预览回调将不会到达 UI 线程(Android 的新版本变得更加嫉妒应用程序劫持主线程CPU 或网络密集型任务)。

其次,考虑使用camera.setPreviewCallbackWithBuffer()来避免不必要的垃圾收集。这种技术的一个额外好处是,如果你只准备一个预览缓冲区,你只会在释放它时收到预览回调。所以,你可以简单地使用代码:

public void onPreviewFrame(byte[] data, Camera camera) {
    long timestampBeforecompression = SystemClock.uptimeMillis();
    compress(data);
    long compressionMillis = SystemClock.uptimeMillis() - timestampBeforecompression;
    SystemClock.sleep(1000000/intFramesPerSecond - compressionMillis);
    camera.addCallbackBuffer(data);
}

也许,如果您还补偿当前的相机帧速率,您可以更精确,但这在谈到 2 或 3 FPS 时可能并不重要。


最后,还有一个提示:许多设备仍然支持已弃用的 setPreviewFrameRate(),甚至声明您可能感兴趣的支持的 FPS 值:

[1, 2, 3, 4, 5, 8, 10, 15, 20, 30]

在我的无名 Snapdragon-801 平板电脑上。