Merge branch 'master' into wrap-crashes-when-start-camera

pull/851/head
Mattia Iavarone 5 years ago committed by GitHub
commit 66d275be21
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/CameraViewTest.java
  2. 19
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/VideoResultTest.java
  3. 3
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/engine/options/Camera1OptionsTest.java
  4. 2
      cameraview/src/androidTest/java/com/otaliastudios/cameraview/preview/MockCameraPreview.java
  5. 3
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraOptions.java
  6. 53
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  7. 14
      cameraview/src/main/java/com/otaliastudios/cameraview/VideoResult.java
  8. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/AudioCodec.java
  9. 8
      cameraview/src/main/java/com/otaliastudios/cameraview/controls/ControlParser.java
  10. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  11. 31
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  12. 15
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraBaseEngine.java
  13. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/CameraEngine.java
  14. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/Snapshot2PictureRecorder.java
  15. 63
      cameraview/src/main/java/com/otaliastudios/cameraview/picture/SnapshotGlPictureRecorder.java
  16. 32
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/CameraPreview.java
  17. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/FilterCameraPreview.java
  18. 19
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  19. 23
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererCameraPreview.java
  20. 6
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/RendererFrameCallback.java
  21. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/SurfaceCameraPreview.java
  22. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/TextureCameraPreview.java
  23. 13
      cameraview/src/main/java/com/otaliastudios/cameraview/video/FullVideoRecorder.java
  24. 33
      cameraview/src/main/java/com/otaliastudios/cameraview/video/SnapshotVideoRecorder.java
  25. 4
      cameraview/src/main/java/com/otaliastudios/cameraview/video/encoding/TextureMediaEncoder.java
  26. 7
      cameraview/src/main/res/values/attrs.xml
  27. 4
      demo/src/main/java/com/otaliastudios/cameraview/demo/CameraActivity.java
  28. 6
      demo/src/main/java/com/otaliastudios/cameraview/demo/Option.java
  29. 2
      demo/src/main/java/com/otaliastudios/cameraview/demo/VideoPreviewActivity.java
  30. 5
      demo/src/main/res/layout/activity_video_preview.xml
  31. 18
      docs/_docs/controls.md

@ -19,6 +19,7 @@ import android.view.View;
import android.view.ViewGroup;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing;
@ -165,6 +166,7 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.getHdr(), controls.getHdr());
assertEquals(cameraView.getAudio(), controls.getAudio());
assertEquals(cameraView.getVideoCodec(), controls.getVideoCodec());
assertEquals(cameraView.getAudioCodec(), controls.getAudioCodec());
assertEquals(cameraView.getPictureFormat(), controls.getPictureFormat());
//noinspection SimplifiableJUnitAssertion
assertEquals(cameraView.getLocation(), null);
@ -773,6 +775,19 @@ public class CameraViewTest extends BaseTest {
assertEquals(cameraView.get(Audio.class), Audio.STEREO);
}
@Test
public void testAudioCodec() {
cameraView.set(AudioCodec.DEVICE_DEFAULT);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.DEVICE_DEFAULT);
cameraView.set(AudioCodec.AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC);
cameraView.set(AudioCodec.HE_AAC);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.HE_AAC);
cameraView.set(AudioCodec.AAC_ELD);
assertEquals(cameraView.get(AudioCodec.class), AudioCodec.AAC_ELD);
}
@Test
public void testVideoCodec() {
cameraView.set(VideoCodec.H_263);

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size;
@ -32,7 +33,8 @@ public class VideoResultTest extends BaseTest {
File file = Mockito.mock(File.class);
int rotation = 90;
Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263;
VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class);
boolean isSnapshot = true;
int maxDuration = 1234;
@ -47,7 +49,8 @@ public class VideoResultTest extends BaseTest {
stub.file = file;
stub.rotation = rotation;
stub.size = size;
stub.videoCodec = codec;
stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location;
stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration;
@ -63,7 +66,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFile(), file);
assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec);
assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);
@ -81,7 +85,8 @@ public class VideoResultTest extends BaseTest {
FileDescriptor fileDescriptor = FileDescriptor.in;
int rotation = 90;
Size size = new Size(20, 120);
VideoCodec codec = VideoCodec.H_263;
VideoCodec videoCodec = VideoCodec.H_263;
AudioCodec audioCodec = AudioCodec.DEVICE_DEFAULT;
Location location = Mockito.mock(Location.class);
boolean isSnapshot = true;
int maxDuration = 1234;
@ -96,7 +101,8 @@ public class VideoResultTest extends BaseTest {
stub.fileDescriptor = fileDescriptor;
stub.rotation = rotation;
stub.size = size;
stub.videoCodec = codec;
stub.videoCodec = videoCodec;
stub.audioCodec = audioCodec;
stub.location = location;
stub.isSnapshot = isSnapshot;
stub.maxDuration = maxDuration;
@ -112,7 +118,8 @@ public class VideoResultTest extends BaseTest {
assertEquals(result.getFileDescriptor(), fileDescriptor);
assertEquals(result.getRotation(), rotation);
assertEquals(result.getSize(), size);
assertEquals(result.getVideoCodec(), codec);
assertEquals(result.getVideoCodec(), videoCodec);
assertEquals(result.getAudioCodec(), audioCodec);
assertEquals(result.getLocation(), location);
assertEquals(result.isSnapshot(), isSnapshot);
assertEquals(result.getMaxSize(), maxFileSize);

@ -7,6 +7,7 @@ import android.hardware.Camera;
import com.otaliastudios.cameraview.BaseTest;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.PictureFormat;
@ -244,10 +245,12 @@ public class Camera1OptionsTest extends BaseTest {
Collection<Grid> grids = o.getSupportedControls(Grid.class);
Collection<VideoCodec> video = o.getSupportedControls(VideoCodec.class);
Collection<AudioCodec> audioCodecs = o.getSupportedControls(AudioCodec.class);
Collection<Mode> sessions = o.getSupportedControls(Mode.class);
Collection<Audio> audio = o.getSupportedControls(Audio.class);
assertEquals(grids.size(), Grid.values().length);
assertEquals(video.size(), VideoCodec.values().length);
assertEquals(audioCodecs.size(), AudioCodec.values().length);
assertEquals(sessions.size(), Mode.values().length);
assertEquals(audio.size(), Audio.values().length);
}

@ -10,7 +10,7 @@ import android.view.ViewGroup;
import com.otaliastudios.cameraview.filter.Filter;
import com.otaliastudios.cameraview.preview.CameraPreview;
public class MockCameraPreview extends FilterCameraPreview<View, Void> {
public class MockCameraPreview extends CameraPreview<View, Void> implements FilterCameraPreview {
public MockCameraPreview(Context context, ViewGroup parent) {
super(context, parent);

@ -4,6 +4,7 @@ package com.otaliastudios.cameraview;
import android.graphics.ImageFormat;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.Engine;
import com.otaliastudios.cameraview.controls.Facing;
@ -105,6 +106,8 @@ public abstract class CameraOptions {
return (Collection<T>) Arrays.asList(Mode.values());
} else if (controlClass.equals(VideoCodec.class)) {
return (Collection<T>) Arrays.asList(VideoCodec.values());
} else if (controlClass.equals(AudioCodec.class)) {
return (Collection<T>) Arrays.asList(AudioCodec.values());
} else if (controlClass.equals(WhiteBalance.class)) {
return (Collection<T>) getSupportedWhiteBalance();
} else if (controlClass.equals(Engine.class)) {

@ -33,6 +33,7 @@ import androidx.lifecycle.LifecycleOwner;
import androidx.lifecycle.OnLifecycleEvent;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Control;
import com.otaliastudios.cameraview.controls.ControlParser;
import com.otaliastudios.cameraview.controls.Engine;
@ -268,6 +269,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(controls.getHdr());
setAudio(controls.getAudio());
setAudioBitRate(audioBitRate);
setAudioCodec(controls.getAudioCodec());
setPictureSize(sizeSelectors.getPictureSizeSelector());
setPictureMetering(pictureMetering);
setPictureSnapshotMetering(pictureSnapshotMetering);
@ -752,12 +754,26 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
* Sets the lifecycle owner for this view. This means you don't need
* to call {@link #open()}, {@link #close()} or {@link #destroy()} at all.
*
* If you want that lifecycle stopped controlling the state of the camera,
* pass null in this method.
*
* @param owner the owner activity or fragment
*/
public void setLifecycleOwner(@NonNull LifecycleOwner owner) {
if (mLifecycle != null) mLifecycle.removeObserver(this);
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
public void setLifecycleOwner(@Nullable LifecycleOwner owner) {
if (owner == null) {
clearLifecycleObserver();
} else {
clearLifecycleObserver();
mLifecycle = owner.getLifecycle();
mLifecycle.addObserver(this);
}
}
private void clearLifecycleObserver() {
if (mLifecycle != null) {
mLifecycle.removeObserver(this);
mLifecycle = null;
}
}
/**
@ -892,6 +908,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setWhiteBalance((WhiteBalance) control);
} else if (control instanceof VideoCodec) {
setVideoCodec((VideoCodec) control);
} else if (control instanceof AudioCodec) {
setAudioCodec((AudioCodec) control);
} else if (control instanceof Preview) {
setPreview((Preview) control);
} else if (control instanceof Engine) {
@ -928,6 +946,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return (T) getWhiteBalance();
} else if (controlClass == VideoCodec.class) {
return (T) getVideoCodec();
} else if (controlClass == AudioCodec.class) {
return (T) getAudioCodec();
} else if (controlClass == Preview.class) {
return (T) getPreview();
} else if (controlClass == Engine.class) {
@ -1000,6 +1020,7 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
setHdr(oldEngine.getHdr());
setAudio(oldEngine.getAudio());
setAudioBitRate(oldEngine.getAudioBitRate());
setAudioCodec(oldEngine.getAudioCodec());
setPictureSize(oldEngine.getPictureSizeSelector());
setPictureFormat(oldEngine.getPictureFormat());
setVideoSize(oldEngine.getVideoSizeSelector());
@ -1629,6 +1650,30 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
return mCameraEngine.getAudioBitRate();
}
/**
* Sets the encoder for audio recordings.
* Defaults to {@link AudioCodec#DEVICE_DEFAULT}.
*
* @see AudioCodec#DEVICE_DEFAULT
* @see AudioCodec#AAC
* @see AudioCodec#HE_AAC
* @see AudioCodec#AAC_ELD
*
* @param codec requested audio codec
*/
public void setAudioCodec(@NonNull AudioCodec codec) {
mCameraEngine.setAudioCodec(codec);
}
/**
* Gets the current encoder for audio recordings.
* @return the current audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return mCameraEngine.getAudioCodec();
}
/**
* Adds a {@link CameraListener} instance to be notified of all
* interesting events that happen during the camera lifecycle.

@ -3,6 +3,7 @@ package com.otaliastudios.cameraview;
import android.location.Location;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.size.Size;
@ -34,6 +35,7 @@ public class VideoResult {
public FileDescriptor fileDescriptor;
public Facing facing;
public VideoCodec videoCodec;
public AudioCodec audioCodec;
public Audio audio;
public long maxSize;
public int maxDuration;
@ -60,6 +62,7 @@ public class VideoResult {
private final FileDescriptor fileDescriptor;
private final Facing facing;
private final VideoCodec videoCodec;
private final AudioCodec audioCodec;
private final Audio audio;
private final long maxSize;
private final int maxDuration;
@ -77,6 +80,7 @@ public class VideoResult {
fileDescriptor = builder.fileDescriptor;
facing = builder.facing;
videoCodec = builder.videoCodec;
audioCodec = builder.audioCodec;
audio = builder.audio;
maxSize = builder.maxSize;
maxDuration = builder.maxDuration;
@ -173,6 +177,16 @@ public class VideoResult {
return videoCodec;
}
/**
* Returns the codec that was used to encode the audio frames.
*
* @return the audio codec
*/
@NonNull
public AudioCodec getAudioCodec() {
return audioCodec;
}
/**
* Returns the max file size in bytes that was set before recording,
* or 0 if no constraint was set.

@ -0,0 +1,63 @@
package com.otaliastudios.cameraview.controls;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import com.otaliastudios.cameraview.CameraView;
/**
* Constants for selecting the encoder of audio recordings.
* https://developer.android.com/guide/topics/media/media-formats.html#audio-formats
*
* @see CameraView#setAudioCodec(AudioCodec)
*/
public enum AudioCodec implements Control {
/**
* Let the device choose its codec.
*/
DEVICE_DEFAULT(0),
/**
* The AAC codec.
*/
AAC(1),
/**
* The HE_AAC codec.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
HE_AAC(2),
/**
* The AAC_ELD codec.
*/
@RequiresApi(Build.VERSION_CODES.JELLY_BEAN)
AAC_ELD(3);
static final AudioCodec DEFAULT = DEVICE_DEFAULT;
private int value;
AudioCodec(int value) {
this.value = value;
}
int value() {
return value;
}
@NonNull
static AudioCodec fromValue(int value) {
AudioCodec[] list = AudioCodec.values();
for (AudioCodec action : list) {
if (action.value() == value) {
return action;
}
}
return DEFAULT;
}
}

@ -22,6 +22,7 @@ public class ControlParser {
private int hdr;
private int audio;
private int videoCodec;
private int audioCodec;
private int engine;
private int pictureFormat;
@ -38,6 +39,8 @@ public class ControlParser {
audio = array.getInteger(R.styleable.CameraView_cameraAudio, Audio.DEFAULT.value());
videoCodec = array.getInteger(R.styleable.CameraView_cameraVideoCodec,
VideoCodec.DEFAULT.value());
audioCodec = array.getInteger(R.styleable.CameraView_cameraAudioCodec,
AudioCodec.DEFAULT.value());
engine = array.getInteger(R.styleable.CameraView_cameraEngine, Engine.DEFAULT.value());
pictureFormat = array.getInteger(R.styleable.CameraView_cameraPictureFormat,
PictureFormat.DEFAULT.value());
@ -84,6 +87,11 @@ public class ControlParser {
return Audio.fromValue(audio);
}
@NonNull
public AudioCodec getAudioCodec() {
return AudioCodec.fromValue(audioCodec);
}
@NonNull
public VideoCodec getVideoCodec() {
return VideoCodec.fromValue(videoCodec);

@ -43,7 +43,7 @@ import com.otaliastudios.cameraview.metering.MeteringTransform;
import com.otaliastudios.cameraview.picture.Full1PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot1PictureRecorder;
import com.otaliastudios.cameraview.picture.SnapshotGlPictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full1VideoRecorder;
@ -220,6 +220,7 @@ public class Camera1Engine extends CameraBaseEngine implements
throw new IllegalStateException("previewStreamSize should not be null at this point.");
}
mPreview.setStreamSize(previewSize.getWidth(), previewSize.getHeight());
mPreview.setDrawRotation(0);
Camera.Parameters params = mCamera.getParameters();
// NV21 should be the default, but let's make sure, since YuvImage will only support this
@ -368,11 +369,12 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakePictureSnapshot:", "executing.");
// Not the real size: it will be cropped to match the view ratio
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
// Actually it will be rotated and set to 0.
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
if (mPreview instanceof GlCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this, (GlCameraPreview) mPreview, outputRatio);
if (mPreview instanceof RendererCameraPreview && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mPictureRecorder = new SnapshotGlPictureRecorder(stub, this,
(RendererCameraPreview) mPreview, outputRatio, getOverlay());
} else {
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT, Axis.RELATIVE_TO_SENSOR);
mPictureRecorder = new Snapshot1PictureRecorder(stub, this, mCamera, outputRatio);
}
mPictureRecorder.take();
@ -408,13 +410,13 @@ public class Camera1Engine extends CameraBaseEngine implements
@Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) {
if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
throw new IllegalStateException("Video snapshots are only supported on API 18+.");
}
GlCameraPreview glPreview = (GlCameraPreview) mPreview;
RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null.");
@ -436,8 +438,7 @@ public class Camera1Engine extends CameraBaseEngine implements
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start.
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview,
getOverlay(), stub.rotation);
mVideoRecorder = new SnapshotVideoRecorder(Camera1Engine.this, glPreview, getOverlay());
mVideoRecorder.start(stub);
}

@ -64,7 +64,7 @@ import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.metering.MeteringRegions;
import com.otaliastudios.cameraview.picture.Full2PictureRecorder;
import com.otaliastudios.cameraview.picture.Snapshot2PictureRecorder;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
import com.otaliastudios.cameraview.size.Size;
import com.otaliastudios.cameraview.video.Full2VideoRecorder;
@ -785,17 +785,16 @@ public class Camera2Engine extends CameraBaseEngine implements
action.start(this);
} else {
LOG.i("onTakePictureSnapshot:", "doMetering is false. Performing.");
if (!(mPreview instanceof GlCameraPreview)) {
if (!(mPreview instanceof RendererCameraPreview)) {
throw new RuntimeException("takePictureSnapshot with Camera2 is only " +
"supported with Preview.GL_SURFACE");
}
// stub.size is not the real size: it will be cropped to the given ratio
// stub.rotation will be set to 0 - we rotate the texture instead.
stub.size = getUncroppedSnapshotSize(Reference.OUTPUT);
stub.rotation = getAngles().offset(Reference.SENSOR, Reference.OUTPUT,
Axis.RELATIVE_TO_SENSOR);
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mPictureRecorder = new Snapshot2PictureRecorder(stub, this,
(GlCameraPreview) mPreview, outputRatio);
(RendererCameraPreview) mPreview, outputRatio);
mPictureRecorder.take();
}
}
@ -910,10 +909,10 @@ public class Camera2Engine extends CameraBaseEngine implements
@Override
protected void onTakeVideoSnapshot(@NonNull VideoResult.Stub stub,
@NonNull AspectRatio outputRatio) {
if (!(mPreview instanceof GlCameraPreview)) {
if (!(mPreview instanceof RendererCameraPreview)) {
throw new IllegalStateException("Video snapshots are only supported with GL_SURFACE.");
}
GlCameraPreview glPreview = (GlCameraPreview) mPreview;
RendererCameraPreview glPreview = (RendererCameraPreview) mPreview;
Size outputSize = getUncroppedSnapshotSize(Reference.OUTPUT);
if (outputSize == null) {
throw new IllegalStateException("outputSize should not be null.");
@ -921,24 +920,10 @@ public class Camera2Engine extends CameraBaseEngine implements
Rect outputCrop = CropHelper.computeCrop(outputSize, outputRatio);
outputSize = new Size(outputCrop.width(), outputCrop.height());
stub.size = outputSize;
// Vertical: 0 (270-0-0)
// Left (unlocked): 270 (270-90-270)
// Right (unlocked): 90 (270-270-90)
// Upside down (unlocked): 180 (270-180-180)
// Left (locked): 270 (270-0-270)
// Right (locked): 90 (270-0-90)
// Upside down (locked): 180 (270-0-180)
// Unlike Camera1, the correct formula seems to be deviceOrientation,
// which means offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE).
stub.rotation = getAngles().offset(Reference.BASE, Reference.OUTPUT, Axis.ABSOLUTE);
stub.rotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
stub.videoFrameRate = Math.round(mPreviewFrameRate);
LOG.i("onTakeVideoSnapshot", "rotation:", stub.rotation, "size:", stub.size);
// Start.
// The overlay rotation should alway be VIEW-OUTPUT, just liek Camera1Engine.
int overlayRotation = getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay(),
overlayRotation);
mVideoRecorder = new SnapshotVideoRecorder(this, glPreview, getOverlay());
mVideoRecorder.start(stub);
}

@ -16,6 +16,7 @@ import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.controls.Flash;
import com.otaliastudios.cameraview.controls.Hdr;
@ -61,6 +62,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
@SuppressWarnings("WeakerAccess") protected Flash mFlash;
@SuppressWarnings("WeakerAccess") protected WhiteBalance mWhiteBalance;
@SuppressWarnings("WeakerAccess") protected VideoCodec mVideoCodec;
@SuppressWarnings("WeakerAccess") protected AudioCodec mAudioCodec;
@SuppressWarnings("WeakerAccess") protected Hdr mHdr;
@SuppressWarnings("WeakerAccess") protected PictureFormat mPictureFormat;
@SuppressWarnings("WeakerAccess") protected Location mLocation;
@ -243,6 +245,17 @@ public abstract class CameraBaseEngine extends CameraEngine {
return mVideoBitRate;
}
@Override
public final void setAudioCodec(@NonNull AudioCodec codec) {
mAudioCodec = codec;
}
@NonNull
@Override
public final AudioCodec getAudioCodec() {
return mAudioCodec;
}
@Override
public final void setAudioBitRate(int audioBitRate) {
mAudioBitRate = audioBitRate;
@ -581,6 +594,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
}
stub.isSnapshot = false;
stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation;
stub.facing = mFacing;
stub.audio = mAudio;
@ -608,6 +622,7 @@ public abstract class CameraBaseEngine extends CameraEngine {
stub.file = file;
stub.isSnapshot = true;
stub.videoCodec = mVideoCodec;
stub.audioCodec = mAudioCodec;
stub.location = mLocation;
stub.facing = mFacing;
stub.videoBitRate = mVideoBitRate;

@ -17,6 +17,7 @@ import com.otaliastudios.cameraview.CameraException;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.CameraOptions;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.PictureFormat;
import com.otaliastudios.cameraview.engine.orchestrator.CameraOrchestrator;
import com.otaliastudios.cameraview.engine.orchestrator.CameraState;
@ -637,6 +638,9 @@ public abstract class CameraEngine implements
public abstract void setAudioBitRate(int audioBitRate);
public abstract int getAudioBitRate();
public abstract void setAudioCodec(@NonNull AudioCodec codec);
@NonNull public abstract AudioCodec getAudioCodec();
public abstract void setSnapshotMaxWidth(int maxWidth);
public abstract int getSnapshotMaxWidth();

@ -18,7 +18,7 @@ import com.otaliastudios.cameraview.engine.action.Actions;
import com.otaliastudios.cameraview.engine.action.BaseAction;
import com.otaliastudios.cameraview.engine.action.CompletionCallback;
import com.otaliastudios.cameraview.engine.lock.LockAction;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.size.AspectRatio;
/**
@ -107,9 +107,9 @@ public class Snapshot2PictureRecorder extends SnapshotGlPictureRecorder {
public Snapshot2PictureRecorder(@NonNull PictureResult.Stub stub,
@NonNull Camera2Engine engine,
@NonNull GlCameraPreview preview,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio) {
super(stub, engine, preview, outputRatio);
super(stub, engine, preview, outputRatio, engine.getOverlay());
mHolder = engine;
mAction = Actions.sequence(

@ -12,14 +12,11 @@ import android.os.Build;
import com.otaliastudios.cameraview.PictureResult;
import com.otaliastudios.cameraview.internal.GlTextureDrawer;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.controls.Facing;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Axis;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.internal.CropHelper;
import com.otaliastudios.cameraview.internal.WorkerHandler;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filter.Filter;
@ -30,13 +27,14 @@ import com.otaliastudios.opengl.surface.EglSurface;
import com.otaliastudios.opengl.surface.EglWindowSurface;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.WorkerThread;
import android.view.Surface;
/**
* API 19.
* Records a picture snapshots from the {@link GlCameraPreview}. It works as follows:
* Records a picture snapshots from the {@link RendererCameraPreview}. It works as follows:
*
* - We register a one time {@link RendererFrameCallback} on the preview
* - We get the textureId and the frame callback on the {@link RendererThread}
@ -55,8 +53,7 @@ import android.view.Surface;
*/
public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
private CameraEngine mEngine;
private GlCameraPreview mPreview;
private RendererCameraPreview mPreview;
private AspectRatio mOutputRatio;
private Overlay mOverlay;
@ -66,14 +63,14 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
public SnapshotGlPictureRecorder(
@NonNull PictureResult.Stub stub,
@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@NonNull AspectRatio outputRatio) {
super(stub, engine);
mEngine = engine;
@Nullable PictureResultListener listener,
@NonNull RendererCameraPreview preview,
@NonNull AspectRatio outputRatio,
@Nullable Overlay overlay) {
super(stub, listener);
mPreview = preview;
mOutputRatio = outputRatio;
mOverlay = engine.getOverlay();
mOverlay = overlay;
mHasOverlay = mOverlay != null && mOverlay.drawsOn(Overlay.Target.PICTURE_SNAPSHOT);
}
@ -96,10 +93,10 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
final float scaleX,
final float scaleY) {
int rotation, float scaleX, float scaleY) {
mPreview.removeRendererFrameCallback(this);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture, scaleX, scaleY);
SnapshotGlPictureRecorder.this.onRendererFrame(surfaceTexture,
rotation, scaleX, scaleY);
}
});
@ -129,18 +126,17 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@RendererThread
@TargetApi(Build.VERSION_CODES.KITKAT)
protected void onRendererFrame(@SuppressWarnings("unused") @NonNull final SurfaceTexture surfaceTexture,
final int rotation,
final float scaleX,
final float scaleY) {
// Get egl context from the RendererThread, which is the one in which we have created
// the textureId and the overlayTextureId, managed by the GlSurfaceView.
// Next operations can then be performed on different threads using this handle.
final EGLContext eglContext = EGL14.eglGetCurrentContext();
// Calling this invalidates the rotation/scale logic below:
// surfaceTexture.getTransformMatrix(mTransform); // TODO activate and fix the logic.
WorkerHandler.execute(new Runnable() {
@Override
public void run() {
takeFrame(surfaceTexture, scaleX, scaleY, eglContext);
takeFrame(surfaceTexture, rotation, scaleX, scaleY, eglContext);
}
});
@ -173,6 +169,7 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@WorkerThread
@TargetApi(Build.VERSION_CODES.KITKAT)
protected void takeFrame(@NonNull SurfaceTexture surfaceTexture,
int rotation,
float scaleX,
float scaleY,
@NonNull EGLContext eglContext) {
@ -189,22 +186,18 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
eglSurface.makeCurrent();
final float[] transform = mTextureDrawer.getTextureTransform();
// 2. Apply scale and crop
boolean flip = mEngine.getAngles().flip(Reference.VIEW, Reference.SENSOR);
float realScaleX = flip ? scaleY : scaleX;
float realScaleY = flip ? scaleX : scaleY;
float scaleTranslX = (1F - realScaleX) / 2F;
float scaleTranslY = (1F - realScaleY) / 2F;
// 2. Apply preview transformations
surfaceTexture.getTransformMatrix(transform);
float scaleTranslX = (1F - scaleX) / 2F;
float scaleTranslY = (1F - scaleY) / 2F;
Matrix.translateM(transform, 0, scaleTranslX, scaleTranslY, 0);
Matrix.scaleM(transform, 0, realScaleX, realScaleY, 1);
Matrix.scaleM(transform, 0, scaleX, scaleY, 1);
// 3. Apply rotation and flip
// If this doesn't work, rotate "rotation" before scaling, like GlCameraPreview does.
Matrix.translateM(transform, 0, 0.5F, 0.5F, 0); // Go back to 0,0
Matrix.rotateM(transform, 0, -mResult.rotation, 0, 0, 1); // Rotate (not sure why we need the minus)
mResult.rotation = 0;
if (mResult.facing == Facing.FRONT) { // 5. Flip horizontally for front camera
Matrix.scaleM(transform, 0, -1, 1, 1);
}
Matrix.rotateM(transform, 0, rotation + mResult.rotation, 0, 0, 1); // Rotate to OUTPUT
Matrix.scaleM(transform, 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.translateM(transform, 0, -0.5F, -0.5F, 0); // Go back to old position
// 4. Do pretty much the same for overlays
@ -213,13 +206,12 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
mOverlayDrawer.draw(Overlay.Target.PICTURE_SNAPSHOT);
// 2. Then we can apply the transformations
int rotation = mEngine.getAngles().offset(Reference.VIEW, Reference.OUTPUT, Axis.ABSOLUTE);
Matrix.translateM(mOverlayDrawer.getTransform(), 0, 0.5F, 0.5F, 0);
Matrix.rotateM(mOverlayDrawer.getTransform(), 0, rotation, 0, 0, 1);
// No need to flip the x axis for front camera, but need to flip the y axis always.
Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1);
Matrix.rotateM(mOverlayDrawer.getTransform(), 0, mResult.rotation, 0, 0, 1);
Matrix.scaleM(mOverlayDrawer.getTransform(), 0, 1, -1, 1); // Vertical flip because we'll use glReadPixels
Matrix.translateM(mOverlayDrawer.getTransform(), 0, -0.5F, -0.5F, 0);
}
mResult.rotation = 0;
// 5. Draw and save
long timestampUs = surfaceTexture.getTimestamp() / 1000L;
@ -239,7 +231,6 @@ public class SnapshotGlPictureRecorder extends SnapshotPictureRecorder {
@Override
protected void dispatchResult() {
mEngine = null;
mOutputRatio = null;
super.dispatchResult();
}

@ -18,6 +18,7 @@ import com.google.android.gms.tasks.TaskCompletionSource;
import com.google.android.gms.tasks.Tasks;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.engine.offset.Reference;
import com.otaliastudios.cameraview.size.Size;
/**
@ -62,18 +63,24 @@ public abstract class CameraPreview<T extends View, Output> {
@VisibleForTesting CropCallback mCropCallback;
private SurfaceCallback mSurfaceCallback;
private T mView;
boolean mCropping;
@SuppressWarnings("WeakerAccess")
protected boolean mCropping;
// These are the surface dimensions in REF_VIEW.
int mOutputSurfaceWidth;
int mOutputSurfaceHeight;
@SuppressWarnings("WeakerAccess")
protected int mOutputSurfaceWidth;
@SuppressWarnings("WeakerAccess")
protected int mOutputSurfaceHeight;
// These are the preview stream dimensions, in REF_VIEW.
int mInputStreamWidth;
int mInputStreamHeight;
@SuppressWarnings("WeakerAccess")
protected int mInputStreamWidth;
@SuppressWarnings("WeakerAccess")
protected int mInputStreamHeight;
// The rotation, if any, to be applied when drawing.
int mDrawRotation;
@SuppressWarnings("WeakerAccess")
protected int mDrawRotation;
/**
* Creates a new preview.
@ -88,7 +95,7 @@ public abstract class CameraPreview<T extends View, Output> {
* Sets a callback to be notified of surface events (creation, change, destruction)
* @param callback a callback
*/
public final void setSurfaceCallback(@Nullable SurfaceCallback callback) {
public void setSurfaceCallback(@Nullable SurfaceCallback callback) {
if (hasSurface() && mSurfaceCallback != null) {
mSurfaceCallback.onSurfaceDestroyed();
}
@ -124,9 +131,8 @@ public abstract class CameraPreview<T extends View, Output> {
* @return the root view
*/
@SuppressWarnings("unused")
@VisibleForTesting
@NonNull
abstract View getRootView();
public abstract View getRootView();
/**
* Returns the output surface object (for example a SurfaceHolder
@ -162,7 +168,7 @@ public abstract class CameraPreview<T extends View, Output> {
* Returns the current input stream size, in view coordinates.
* @return the current input stream size
*/
@SuppressWarnings("unused")
@VisibleForTesting
@NonNull
final Size getStreamSize() {
return new Size(mInputStreamWidth, mInputStreamHeight);
@ -326,11 +332,15 @@ public abstract class CameraPreview<T extends View, Output> {
* Sometimes we don't need this:
* - In Camera1, the buffer producer sets our Surface size and rotates it based on the value
* that we pass to {@link android.hardware.Camera.Parameters#setDisplayOrientation(int)},
* so the stream that comes in is already rotated.
* so the stream that comes in is already rotated (if we apply SurfaceTexture transform).
* - In Camera2, for {@link android.view.SurfaceView} based previews, apparently it just works
* out of the box. The producer might be doing something similar.
*
* But in all the other Camera2 cases, we need to apply this rotation when drawing the surface.
* Seems that Camera1 can correctly rotate the stream/transform to {@link Reference#VIEW},
* while Camera2, that does not have any rotation API, will only rotate to {@link Reference#BASE}.
* That's why in Camera2 this angle is set as the offset between BASE and VIEW.
*
* @param drawRotation the rotation in degrees
*/
public void setDrawRotation(int drawRotation) {

@ -16,18 +16,13 @@ import com.otaliastudios.cameraview.filter.Filter;
* The preview has the responsibility of calling {@link Filter#setSize(int, int)}
* whenever the preview size changes and as soon as the filter is applied.
*/
public abstract class FilterCameraPreview<T extends View, Output> extends CameraPreview<T, Output> {
@SuppressWarnings("WeakerAccess")
public FilterCameraPreview(@NonNull Context context, @NonNull ViewGroup parent) {
super(context, parent);
}
public interface FilterCameraPreview {
/**
* Sets a new filter.
* @param filter new filter
*/
public abstract void setFilter(@NonNull Filter filter);
void setFilter(@NonNull Filter filter);
/**
* Returns the currently used filter.
@ -35,5 +30,5 @@ public abstract class FilterCameraPreview<T extends View, Output> extends Camera
*/
@SuppressWarnings("unused")
@NonNull
public abstract Filter getCurrentFilter();
Filter getCurrentFilter();
}

@ -60,7 +60,8 @@ import javax.microedition.khronos.opengles.GL10;
* which means that we can fetch the GL context that was created and is managed
* by the {@link GLSurfaceView}.
*/
public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceTexture> {
public class GlCameraPreview extends CameraPreview<GLSurfaceView, SurfaceTexture>
implements FilterCameraPreview, RendererCameraPreview {
private boolean mDispatched;
private SurfaceTexture mInputSurfaceTexture;
@ -110,7 +111,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@NonNull
@Override
View getRootView() {
public View getRootView() {
return mRootView;
}
@ -232,7 +233,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
mOutputTextureDrawer.draw(mInputSurfaceTexture.getTimestamp() / 1000L);
for (RendererFrameCallback callback : mRendererFrameCallbacks) {
callback.onRendererFrame(mInputSurfaceTexture, mCropScaleX, mCropScaleY);
callback.onRendererFrame(mInputSurfaceTexture, mDrawRotation, mCropScaleX, mCropScaleY);
}
}
}
@ -289,11 +290,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
if (callback != null) callback.onCrop();
}
/**
* Method specific to the GL preview. Adds a {@link RendererFrameCallback}
* to receive renderer frame events.
* @param callback a callback
*/
@Override
public void addRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
getView().queueEvent(new Runnable() {
@Override
@ -308,11 +305,7 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
});
}
/**
* Method specific to the GL preview. Removes a {@link RendererFrameCallback}
* that was previously added to receive renderer frame events.
* @param callback a callback
*/
@Override
public void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback) {
mRendererFrameCallbacks.remove(callback);
}

@ -0,0 +1,23 @@
package com.otaliastudios.cameraview.preview;
import androidx.annotation.NonNull;
/**
* Base interface for previews that support renderer frame callbacks,
* see {@link RendererFrameCallback}.
*/
public interface RendererCameraPreview {
/**
* Adds a {@link RendererFrameCallback} to receive renderer frame events.
* @param callback a callback
*/
void addRendererFrameCallback(@NonNull final RendererFrameCallback callback);
/**
* Removes a {@link RendererFrameCallback} that was previously added to receive renderer
* frame events.
* @param callback a callback
*/
void removeRendererFrameCallback(@NonNull final RendererFrameCallback callback);
}

@ -24,17 +24,17 @@ public interface RendererFrameCallback {
* Called on the renderer thread after each frame was drawn.
* You are not supposed to hold for too long onto this thread, because
* well, it is the rendering thread.
*
* @param surfaceTexture the texture to get transformation
* @param rotation the rotation (to reach REF_VIEW)
* @param scaleX the scaleX (in REF_VIEW) value
* @param scaleY the scaleY (in REF_VIEW) value
*/
@RendererThread
void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, float scaleX, float scaleY);
void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, int rotation, float scaleX, float scaleY);
/**
* Called when the renderer filter changes. This is guaranteed to be called at least once
* before the first {@link #onRendererFrame(SurfaceTexture, float, float)}.
* before the first {@link #onRendererFrame(SurfaceTexture, int, float, float)}.
*
* @param filter the new filter
*/

@ -75,7 +75,7 @@ public class SurfaceCameraPreview extends CameraPreview<SurfaceView, SurfaceHold
@NonNull
@Override
View getRootView() {
public View getRootView() {
return mRootView;
}

@ -68,7 +68,7 @@ public class TextureCameraPreview extends CameraPreview<TextureView, SurfaceText
@NonNull
@Override
View getRootView() {
public View getRootView() {
return mRootView;
}

@ -2,10 +2,12 @@ package com.otaliastudios.cameraview.video;
import android.media.CamcorderProfile;
import android.media.MediaRecorder;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.controls.VideoCodec;
import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.internal.CamcorderProfiles;
@ -13,6 +15,7 @@ import com.otaliastudios.cameraview.size.Size;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
/**
* A {@link VideoRecorder} that uses {@link android.media.MediaRecorder} APIs.
@ -105,6 +108,16 @@ public abstract class FullVideoRecorder extends VideoRecorder {
mProfile.videoCodec = MediaRecorder.VideoEncoder.H263;
mProfile.fileFormat = MediaRecorder.OutputFormat.MPEG_4; // should work
}
// Set audio codec if the user has specified a specific codec.
if (stub.audioCodec == AudioCodec.AAC) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.AAC;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& stub.audioCodec == AudioCodec.HE_AAC) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.HE_AAC;
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
&& stub.audioCodec == AudioCodec.AAC_ELD) {
mProfile.audioCodec = MediaRecorder.AudioEncoder.AAC_ELD;
}
mMediaRecorder.setOutputFormat(mProfile.fileFormat);
// 4. Update the VideoResult stub with information from the profile, if the

@ -1,17 +1,20 @@
package com.otaliastudios.cameraview.video;
import android.graphics.SurfaceTexture;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.opengl.EGL14;
import android.os.Build;
import com.otaliastudios.cameraview.CameraLogger;
import com.otaliastudios.cameraview.controls.AudioCodec;
import com.otaliastudios.cameraview.internal.DeviceEncoders;
import com.otaliastudios.cameraview.overlay.Overlay;
import com.otaliastudios.cameraview.VideoResult;
import com.otaliastudios.cameraview.controls.Audio;
import com.otaliastudios.cameraview.engine.CameraEngine;
import com.otaliastudios.cameraview.overlay.OverlayDrawer;
import com.otaliastudios.cameraview.preview.GlCameraPreview;
import com.otaliastudios.cameraview.preview.RendererCameraPreview;
import com.otaliastudios.cameraview.preview.RendererFrameCallback;
import com.otaliastudios.cameraview.preview.RendererThread;
import com.otaliastudios.cameraview.filter.Filter;
@ -52,7 +55,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private MediaEncoderEngine mEncoderEngine;
private final Object mEncoderEngineLock = new Object();
private GlCameraPreview mPreview;
private RendererCameraPreview mPreview;
private int mCurrentState = STATE_NOT_RECORDING;
private int mDesiredState = STATE_NOT_RECORDING;
@ -61,19 +64,16 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
private Overlay mOverlay;
private OverlayDrawer mOverlayDrawer;
private boolean mHasOverlay;
private int mOverlayRotation;
private Filter mCurrentFilter;
public SnapshotVideoRecorder(@NonNull CameraEngine engine,
@NonNull GlCameraPreview preview,
@Nullable Overlay overlay,
int overlayRotation) {
@NonNull RendererCameraPreview preview,
@Nullable Overlay overlay) {
super(engine);
mPreview = preview;
mOverlay = overlay;
mHasOverlay = overlay != null && overlay.drawsOn(Overlay.Target.VIDEO_SNAPSHOT);
mOverlayRotation = overlayRotation;
}
@Override
@ -126,9 +126,8 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
@RendererThread
@Override
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture,
float scaleX,
float scaleY) {
public void onRendererFrame(@NonNull SurfaceTexture surfaceTexture, int rotation,
float scaleX, float scaleY) {
if (mCurrentState == STATE_NOT_RECORDING && mDesiredState == STATE_RECORDING) {
LOG.i("Starting the encoder engine.");
@ -145,7 +144,14 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
case H_264: videoType = "video/avc"; break; // MediaFormat.MIMETYPE_VIDEO_AVC:
case DEVICE_DEFAULT: videoType = "video/avc"; break;
}
String audioType = "audio/mp4a-latm";
String audioType = "";
switch (mResult.audioCodec) {
case AAC:
case HE_AAC:
case AAC_ELD: audioType = "audio/mp4a-latm"; break; // MediaFormat.MIMETYPE_AUDIO_AAC:
case DEVICE_DEFAULT: audioType = "audio/mp4a-latm"; break;
}
TextureConfig videoConfig = new TextureConfig();
AudioConfig audioConfig = new AudioConfig();
@ -219,7 +225,7 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
videoConfig.height = mResult.size.getHeight();
videoConfig.bitRate = mResult.videoBitRate;
videoConfig.frameRate = mResult.videoFrameRate;
videoConfig.rotation = mResult.rotation;
videoConfig.rotation = rotation + mResult.rotation;
videoConfig.mimeType = videoType;
videoConfig.encoder = deviceEncoders.getVideoEncoder();
videoConfig.textureId = mTextureId;
@ -232,7 +238,8 @@ public class SnapshotVideoRecorder extends VideoRecorder implements RendererFram
if (mHasOverlay) {
videoConfig.overlayTarget = Overlay.Target.VIDEO_SNAPSHOT;
videoConfig.overlayDrawer = mOverlayDrawer;
videoConfig.overlayRotation = mOverlayRotation;
videoConfig.overlayRotation = mResult.rotation;
// ^ no "rotation" here! Overlays are already in VIEW ref.
}
TextureMediaEncoder videoEncoder = new TextureMediaEncoder(videoConfig);

@ -218,8 +218,8 @@ public class TextureMediaEncoder extends VideoMediaEncoder<TextureConfig> {
mConfig.overlayDrawer.draw(mConfig.overlayTarget);
Matrix.translateM(mConfig.overlayDrawer.getTransform(),
0, 0.5F, 0.5F, 0);
Matrix.rotateM(mConfig.overlayDrawer.getTransform(), 0, mConfig.overlayRotation,
0, 0, 1);
Matrix.rotateM(mConfig.overlayDrawer.getTransform(),
0, mConfig.overlayRotation, 0, 0, 1);
Matrix.translateM(mConfig.overlayDrawer.getTransform(),
0, -0.5F, -0.5F, 0);
}

@ -141,6 +141,13 @@
<enum name="h264" value="2" />
</attr>
<attr name="cameraAudioCodec" format="enum">
<enum name="deviceDefault" value="0" />
<enum name="aac" value="1" />
<enum name="heAac" value="2" />
<enum name="aacEld" value="3" />
</attr>
<attr name="cameraAutoFocusResetDelay" format="integer|reference"/>
<attr name="cameraAutoFocusMarker" format="string|reference"/>

@ -123,7 +123,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
new Option.PictureMetering(), new Option.PictureSnapshotMetering(),
new Option.PictureFormat(),
// Video recording
new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(),
new Option.PreviewFrameRate(), new Option.VideoCodec(), new Option.Audio(), new Option.AudioCodec(),
// Gestures
new Option.Pinch(), new Option.HorizontalScroll(), new Option.VerticalScroll(),
new Option.Tap(), new Option.LongTap(),
@ -144,7 +144,7 @@ public class CameraActivity extends AppCompatActivity implements View.OnClickLis
// Some controls
false, false, false, false, false, true,
// Video recording
false, false, true,
false, false, false, true,
// Gestures
false, false, false, false, true,
// Watermarks

@ -308,6 +308,12 @@ public abstract class Option<T> {
}
}
public static class AudioCodec extends ControlOption<com.otaliastudios.cameraview.controls.AudioCodec> {
public AudioCodec() {
super(com.otaliastudios.cameraview.controls.AudioCodec.class, "Audio Codec");
}
}
public static class Audio extends ControlOption<com.otaliastudios.cameraview.controls.Audio> {
public Audio() {
super(com.otaliastudios.cameraview.controls.Audio.class, "Audio");

@ -59,6 +59,7 @@ public class VideoPreviewActivity extends AppCompatActivity {
final MessageView audio = findViewById(R.id.audio);
final MessageView audioBitRate = findViewById(R.id.audioBitRate);
final MessageView videoCodec = findViewById(R.id.videoCodec);
final MessageView audioCodec = findViewById(R.id.audioCodec);
final MessageView videoBitRate = findViewById(R.id.videoBitRate);
final MessageView videoFrameRate = findViewById(R.id.videoFrameRate);
@ -69,6 +70,7 @@ public class VideoPreviewActivity extends AppCompatActivity {
audio.setTitleAndMessage("Audio", result.getAudio().name());
audioBitRate.setTitleAndMessage("Audio bit rate", result.getAudioBitRate() + " bits per sec.");
videoCodec.setTitleAndMessage("VideoCodec", result.getVideoCodec().name());
audioCodec.setTitleAndMessage("AudioCodec", result.getAudioCodec().name());
videoBitRate.setTitleAndMessage("Video bit rate", result.getVideoBitRate() + " bits per sec.");
videoFrameRate.setTitleAndMessage("Video frame rate", result.getVideoFrameRate() + " fps");
MediaController controller = new MediaController(this);

@ -39,6 +39,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/audioCodec"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<com.otaliastudios.cameraview.demo.MessageView
android:id="@+id/videoCodec"
android:layout_width="match_parent"

@ -70,6 +70,24 @@ cameraView.setVideoCodec(VideoCodec.H_263);
cameraView.setVideoCodec(VideoCodec.H_264);
```
##### cameraAudioCodec
Sets the audio encoder for video recordings. Defaults to `DEVICE_DEFAULT`,
which should typically be AAC.
The available values are exposed through the `CameraOptions` object.
`AudioCodec.HE_AAC` and `AudioCodec.AAC_ELD` require at least JellyBean.
The library will safely fall back to device default if the min API requirements
are not met.
```java
cameraView.setAudioCodec(AudioCodec.DEVICE_DEFAULT);
cameraView.setAudioCodec(AudioCodec.AAC);
cameraView.setAudioCodec(AudioCodec.HE_AAC);
cameraView.setAudioCodec(AudioCodec.AAC_ELD);
```
##### cameraWhiteBalance
Sets the desired white balance for the current session.

Loading…
Cancel
Save