Search code examples
android-camerasurfaceviewframe-ratemjpeg

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


UPDATED 28 October 2015 to reflect current progress. I have an application that allows the user to set camera parameters for Motion JPEG recording, create an MJPEG file, and then the user can modify those settings and create another file with the updated settings. I am having a problem updating the frames per second setting when the initial value is something other than 30 FPS. When the initial value is 30 FPS, I can update to a different FPS level and sucessfully record a video AT THAT LEVEL. However, I cannot update from a level that is not equal to 30FPS to another FPM level. I get a crash with LogCat showing a problem at

camera.setParameters(parameters);

Full LogCat of error is below,

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)

Checking the LogCat cited line 5 and 6, these correspond to:

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

Below is the application. I'll also include the layout .xml file for reference, as well as a screenshot for grounding.

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); 
  } 
}

Layout 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>

Screenshot: Showing fields, buttons, and surface config

Resolution: It appears that much of the above erratic behaviour may be related to the primary testing device which is a Panasonic Toughpad JT-B1. Running

.getSupportedPreviewFpsRange(); 

on this device returns a range of 8,000-30,000 fps. However, many values in this range result in crashes, and some values outside of this range appear to work fine. Testing a Samsung S4 Active resulted in none of these inconsistencies, with all of the values in the returned range (4,000 - 30,000) working fine, and no tested values outside of this range demonstrating any functionality as expected.


Solution

  • Camera API does not allow to set the preview FPS range to arbitrary values. You are supposed to query the camera parameters for the list of supported ranges, and any other combination is not guaranteed to work.

    In principle, using unsupported values for Camera.setParameters() is undefined behavior. Different devices will fail or work differently when you try the same inputs.

    Definitely, though, you should stop preview to change camera parameters, and restart the preview after that.

    Other than that, you probably can use a workaround to hold on to supported parameters. To achieve 2 fps, and switch to 10 fps, you don't need to change camera settings. Your logic can filter out relevant frames in your onPreviewFrame() by timestamp.


    Furthermore, your code is suboptimal when it comes to preview callbacks. First of all, you should open the camera on a separate handler thread, then the preview callbacks will not arrive on the UI thread (the newer versions of Android become even more jealous about apps hijacking the Main thread for CPU or network intensive tasks).

    Second, consider using camera.setPreviewCallbackWithBuffer() to avoid unnecessary garbage collection. An extra advantage of this technique is that if you only prepare one preview buffer, you will only receive preview callbacks when you release it. So, you can simply use the code:

    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);
    }
    

    Maybe, you can be more precise if you also compensate for the current camera frame rate, but this is probably not critical when speaking about 2 or 3 FPS.


    Finally, there is another hint: many devices still support the deprecated setPreviewFrameRate(), and even declare the supported FPS values that may be of interest for you:

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

    on my no-name Snapdragon-801 tablet.