Permission flag (#718)

* Website changes

* Better preview output class support

* Add cameraRequestPermissions XML attribute

* Website colors
pull/725/head
Mattia Iavarone 5 years ago committed by GitHub
parent ce9ca64370
commit 840e05a3e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 3
      README.md
  2. 11
      cameraview/src/main/java/com/otaliastudios/cameraview/CameraView.java
  3. 9
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera1Engine.java
  4. 5
      cameraview/src/main/java/com/otaliastudios/cameraview/engine/Camera2Engine.java
  5. 2
      cameraview/src/main/java/com/otaliastudios/cameraview/preview/GlCameraPreview.java
  6. 3
      cameraview/src/main/res/values/attrs.xml
  7. 2
      docs/_config.yml
  8. 2
      docs/_layouts/page.html
  9. 62
      docs/_posts/2018-12-20-changelog.md
  10. 11
      docs/_posts/2018-12-20-runtime-permissions.md
  11. 76
      docs/css/main.css
  12. 2
      docs/css/syntax.css
  13. 2
      docs/index.md

@ -117,6 +117,8 @@ Using CameraView is extremely simple:
app:cameraFrameProcessingMaxWidth="@integer/processing_max_width" app:cameraFrameProcessingMaxWidth="@integer/processing_max_width"
app:cameraFrameProcessingMaxHeight="@integer/processing_max_height" app:cameraFrameProcessingMaxHeight="@integer/processing_max_height"
app:cameraFrameProcessingFormat="@integer/processing_format" app:cameraFrameProcessingFormat="@integer/processing_format"
app:cameraFrameProcessingPoolSize="@integer/processing_pool_size"
app:cameraFrameProcessingExecutors="@integer/processing_executors"
app:cameraVideoBitRate="@integer/video_bit_rate" app:cameraVideoBitRate="@integer/video_bit_rate"
app:cameraAudioBitRate="@integer/audio_bit_rate" app:cameraAudioBitRate="@integer/audio_bit_rate"
app:cameraGestureTap="none|autoFocus|takePicture" app:cameraGestureTap="none|autoFocus|takePicture"
@ -146,6 +148,7 @@ Using CameraView is extremely simple:
app:cameraPictureMetering="true|false" app:cameraPictureMetering="true|false"
app:cameraPictureSnapshotMetering="false|true" app:cameraPictureSnapshotMetering="false|true"
app:cameraPictureFormat="jpeg|dng" app:cameraPictureFormat="jpeg|dng"
app:cameraRequestPermissions="true|false"
app:cameraExperimental="false|true"> app:cameraExperimental="false|true">
<!-- Watermark! --> <!-- Watermark! -->

@ -116,12 +116,14 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
final static boolean DEFAULT_USE_DEVICE_ORIENTATION = true; final static boolean DEFAULT_USE_DEVICE_ORIENTATION = true;
final static boolean DEFAULT_PICTURE_METERING = true; final static boolean DEFAULT_PICTURE_METERING = true;
final static boolean DEFAULT_PICTURE_SNAPSHOT_METERING = false; final static boolean DEFAULT_PICTURE_SNAPSHOT_METERING = false;
final static boolean DEFAULT_REQUEST_PERMISSIONS = true;
final static int DEFAULT_FRAME_PROCESSING_POOL_SIZE = 2; final static int DEFAULT_FRAME_PROCESSING_POOL_SIZE = 2;
final static int DEFAULT_FRAME_PROCESSING_EXECUTORS = 1; final static int DEFAULT_FRAME_PROCESSING_EXECUTORS = 1;
// Self managed parameters // Self managed parameters
private boolean mPlaySounds; private boolean mPlaySounds;
private boolean mUseDeviceOrientation; private boolean mUseDeviceOrientation;
private boolean mRequestPermissions;
private HashMap<Gesture, GestureAction> mGestureMap = new HashMap<>(4); private HashMap<Gesture, GestureAction> mGestureMap = new HashMap<>(4);
private Preview mPreview; private Preview mPreview;
private Engine mEngine; private Engine mEngine;
@ -186,6 +188,8 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
boolean useDeviceOrientation = a.getBoolean( boolean useDeviceOrientation = a.getBoolean(
R.styleable.CameraView_cameraUseDeviceOrientation, DEFAULT_USE_DEVICE_ORIENTATION); R.styleable.CameraView_cameraUseDeviceOrientation, DEFAULT_USE_DEVICE_ORIENTATION);
mExperimental = a.getBoolean(R.styleable.CameraView_cameraExperimental, false); mExperimental = a.getBoolean(R.styleable.CameraView_cameraExperimental, false);
mRequestPermissions = a.getBoolean(R.styleable.CameraView_cameraRequestPermissions,
DEFAULT_REQUEST_PERMISSIONS);
mPreview = controls.getPreview(); mPreview = controls.getPreview();
mEngine = controls.getEngine(); mEngine = controls.getEngine();
@ -779,11 +783,14 @@ public class CameraView extends FrameLayout implements LifecycleObserver {
needsAudio = needsAudio && c.checkSelfPermission(Manifest.permission.RECORD_AUDIO) needsAudio = needsAudio && c.checkSelfPermission(Manifest.permission.RECORD_AUDIO)
!= PackageManager.PERMISSION_GRANTED; != PackageManager.PERMISSION_GRANTED;
if (needsCamera || needsAudio) { if (!needsCamera && !needsAudio) {
return true;
} else if (mRequestPermissions) {
requestPermissions(needsCamera, needsAudio); requestPermissions(needsCamera, needsAudio);
return false; return false;
} else {
return false;
} }
return true;
} }
/** /**

@ -175,12 +175,11 @@ public class Camera1Engine extends CameraBaseEngine implements
@Override @Override
protected Task<Void> onStartBind() { protected Task<Void> onStartBind() {
LOG.i("onStartBind:", "Started"); LOG.i("onStartBind:", "Started");
Object output = mPreview.getOutput();
try { try {
if (output instanceof SurfaceHolder) { if (mPreview.getOutputClass() == SurfaceHolder.class) {
mCamera.setPreviewDisplay((SurfaceHolder) output); mCamera.setPreviewDisplay((SurfaceHolder) mPreview.getOutput());
} else if (output instanceof SurfaceTexture) { } else if (mPreview.getOutputClass() == SurfaceTexture.class) {
mCamera.setPreviewTexture((SurfaceTexture) output); mCamera.setPreviewTexture((SurfaceTexture) mPreview.getOutput());
} else { } else {
throw new RuntimeException("Unknown CameraPreview output class."); throw new RuntimeException("Unknown CameraPreview output class.");
} }

@ -480,8 +480,9 @@ public class Camera2Engine extends CameraBaseEngine implements
// 1. PREVIEW // 1. PREVIEW
// Create a preview surface with the correct size. // Create a preview surface with the correct size.
final Class outputClass = mPreview.getOutputClass();
final Object output = mPreview.getOutput(); final Object output = mPreview.getOutput();
if (output instanceof SurfaceHolder) { if (outputClass == SurfaceHolder.class) {
try { try {
// This must be called from the UI thread... // This must be called from the UI thread...
Tasks.await(Tasks.call(new Callable<Void>() { Tasks.await(Tasks.call(new Callable<Void>() {
@ -497,7 +498,7 @@ public class Camera2Engine extends CameraBaseEngine implements
throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT); throw new CameraException(e, CameraException.REASON_FAILED_TO_CONNECT);
} }
mPreviewStreamSurface = ((SurfaceHolder) output).getSurface(); mPreviewStreamSurface = ((SurfaceHolder) output).getSurface();
} else if (output instanceof SurfaceTexture) { } else if (outputClass == SurfaceTexture.class) {
((SurfaceTexture) output).setDefaultBufferSize( ((SurfaceTexture) output).setDefaultBufferSize(
mPreviewStreamSize.getWidth(), mPreviewStreamSize.getWidth(),
mPreviewStreamSize.getHeight()); mPreviewStreamSize.getHeight());

@ -95,13 +95,13 @@ public class GlCameraPreview extends FilterCameraPreview<GLSurfaceView, SurfaceT
@Override @Override
public void surfaceDestroyed(SurfaceHolder holder) { public void surfaceDestroyed(SurfaceHolder holder) {
dispatchOnSurfaceDestroyed();
glView.queueEvent(new Runnable() { glView.queueEvent(new Runnable() {
@Override @Override
public void run() { public void run() {
renderer.onSurfaceDestroyed(); renderer.onSurfaceDestroyed();
} }
}); });
dispatchOnSurfaceDestroyed();
mDispatched = false; mDispatched = false;
} }
}); });

@ -156,7 +156,8 @@
<enum name="dng" value="1" /> <enum name="dng" value="1" />
</attr> </attr>
<attr name="cameraExperimental" format="boolean" /> <attr name="cameraRequestPermissions" format="boolean|reference"/>
<attr name="cameraExperimental" format="boolean|reference" />
</declare-styleable> </declare-styleable>

@ -3,7 +3,7 @@
# Source site: http://bruth.github.io/jekyll-docs-template/ # Source site: http://bruth.github.io/jekyll-docs-template/
# Ref guide: https://visualstudiomagazine.com/Articles/2015/03/01/GitHub-Pages.aspx?Page=2 # Ref guide: https://visualstudiomagazine.com/Articles/2015/03/01/GitHub-Pages.aspx?Page=2
title: CameraView v2 title: CameraView
# subtitle is used by ourselves, description is used by seo tag. # subtitle is used by ourselves, description is used by seo tag.
subtitle: A well documented, high-level Android interface that makes capturing pictures and videos easy, addressing all of the common issues and needs. subtitle: A well documented, high-level Android interface that makes capturing pictures and videos easy, addressing all of the common issues and needs.

@ -4,7 +4,7 @@ layout: default
<div class="page-header"> <div class="page-header">
<h2>{{ page.title }} <h2>{{ page.title }}
{% if page.subtitle %}<small>{{ page.subtitle }}</small>{% endif %} {% if page.subtitle %}<small class="page-subtitle">{{ page.subtitle }}</small>{% endif %}
</h2> </h2>
</div> </div>

@ -13,13 +13,13 @@ Companies can share a tiny part of their revenue and get private support hours i
## v2.5.0 ## v2.5.0
- [*Camera2*] New: support for RAW pictures with new APIs `setPictureFormat()` and `CameraOptions.getSupportedPictureFormats()`. Contains a **breaking change**: `PictureResult.getFormat()` is not an integer anymore but rather a `PictureFormat`. This API had no real purpose so this might not affect you ([#691][691]) - <small>[Camera2]</small> New: support for RAW pictures with new APIs `setPictureFormat()` and `CameraOptions.getSupportedPictureFormats()`. Contains a **breaking change**: `PictureResult.getFormat()` is not an integer anymore but rather a `PictureFormat`. This API had no real purpose so this might not affect you ([#691][691])
- [*Camera2*] New: support for constraining the frame processing size through `setFrameProcessingMaxWidth()` and `setFrameProcessingMaxHeight()`. This can improve processing performance ([#691][691]) - <small>[Camera2]</small> New: support for constraining the frame processing size through `setFrameProcessingMaxWidth()` and `setFrameProcessingMaxHeight()`. This can improve processing performance ([#691][691])
- [*Camera2*] New: support for choosing the frame processing format through `setFrameProcessingFormat()` and `CameraOptions.getSupportedFrameProcessingFormats()` ([#691][691]) - <small>[Camera2]</small> New: support for choosing the frame processing format through `setFrameProcessingFormat()` and `CameraOptions.getSupportedFrameProcessingFormats()` ([#691][691])
- [*Camera2*] Improvement: Frame processing FPS for Camera2 is now smooth and typically better than Camera1. This required some **breaking changes** (see below) ([#691][691]) - <small>[Camera2]</small> Improvement: Frame processing FPS for Camera2 is now smooth and typically better than Camera1. This required some **breaking changes** (see below) ([#691][691])
- [*Camera1, Camera2*] Improvement: improved internal threading ([#697][697]) - <small>[Camera1, Camera2]</small> Improvement: improved internal threading ([#697][697])
- [*Camera1, Camera2*] Improvement: improvements to stability and edge cases behavior ([#696][696]) - <small>[Camera1, Camera2]</small> Improvement: improvements to stability and edge cases behavior ([#696][696])
- [*Real time filters*] Change: filters do not need the experimental flag anymore ([#691][691]) - <small>[Real time filters]</small> Change: filters do not need the experimental flag anymore ([#691][691])
The new frame processing approach will force you to update your code, because `Frame.getData()` is The new frame processing approach will force you to update your code, because `Frame.getData()` is
not a a byte[] anymore. The class of this object now depends on the engine being used. You can use not a a byte[] anymore. The class of this object now depends on the engine being used. You can use
@ -36,47 +36,47 @@ https://github.com/natario1/CameraView/compare/v2.4.0...v2.5.0
## v2.4.0 ## v2.4.0
- [*Camera2*] New: support for `previewFrameRate`. Controls preview FPS, snapshot FPS, processor FPS, thanks to [@vaibhavbhandula][vaibhavbhandula] ([#653][653]) - <small>[Camera2]</small> New: support for `previewFrameRate`. Controls preview FPS, snapshot FPS, processor FPS, thanks to [@vaibhavbhandula][vaibhavbhandula] ([#653][653])
- [*Camera1*] New: support for `previewFrameRate` for Camera1 ([#661][661]) - <small>[Camera1]</small> New: support for `previewFrameRate` for Camera1 ([#661][661])
- [*Camera2*] Fix: fix crashes when taking snapshots very early ([#651][651]) - <small>[Camera2]</small> Fix: fix crashes when taking snapshots very early ([#651][651])
- [*Preview*] Fix: Fixed preview being upside-down for 180 degrees flips ([#651][651]) - <small>[Preview]</small> Fix: Fixed preview being upside-down for 180 degrees flips ([#651][651])
- Fix: other bug fixes ([#651][651]) - Fix: other bug fixes ([#651][651])
https://github.com/natario1/CameraView/compare/v2.3.1...v2.4.0 https://github.com/natario1/CameraView/compare/v2.3.1...v2.4.0
### v2.3.1 ### v2.3.1
- [*Video*] Improvement: better timing for `onVideoRecordingStart()` thanks to [@agrawalsuneet][agrawalsuneet] ([#632][632]) - <small>[Video]</small> Improvement: better timing for `onVideoRecordingStart()` thanks to [@agrawalsuneet][agrawalsuneet] ([#632][632])
- [*Video, Camera1*] Fix: fixed video errors when starting on specific devices ([#617][617]) - <small>[Video, Camera1]</small> Fix: fixed video errors when starting on specific devices ([#617][617])
- [*Video*] Fix: fixed crash when closing the app during video snapshots ([#630][630]) - <small>[Video]</small> Fix: fixed crash when closing the app during video snapshots ([#630][630])
- [*Preview*] Fix: fixed crash when using `GL_SURFACE` ([#630][630]) - <small>[Preview]</small> Fix: fixed crash when using `GL_SURFACE` ([#630][630])
https://github.com/natario1/CameraView/compare/v2.3.0...v2.3.1 https://github.com/natario1/CameraView/compare/v2.3.0...v2.3.1
## v2.3.0 ## v2.3.0
- [*Camera2, Metering*] New: `startAutoFocus` is much more powerful and does 3A metering (AF, AE, AWB) ([#574][574]) - <small>[Camera2, Metering]</small> New: `startAutoFocus` is much more powerful and does 3A metering (AF, AE, AWB) ([#574][574])
- [*Camera2, Metering*] New: `setPictureMetering(boolean)` decides whether to do metering before `takePicture()`. Defaults to true to improve quality. ([#580][580]) - <small>[Camera2, Metering]</small> New: `setPictureMetering(boolean)` decides whether to do metering before `takePicture()`. Defaults to true to improve quality. ([#580][580])
- [*Camera2, Metering*] New: `setPictureSnapshotMetering(boolean)` decides whether to do metering before `takePictureSnapshot()`. Defaults to false to improve latency. However, you can set this to true to greatly improve the snapshot quality, for example to support `Flash`. ([#580][580]) - <small>[Camera2, Metering]</small> New: `setPictureSnapshotMetering(boolean)` decides whether to do metering before `takePictureSnapshot()`. Defaults to false to improve latency. However, you can set this to true to greatly improve the snapshot quality, for example to support `Flash`. ([#580][580])
- [*Camera2, Metering*] New: metering extended to many more cameras, which can now use `startAutoFocus` or the focus gesture ([#574][574]) - <small>[Camera2, Metering]</small> New: metering extended to many more cameras, which can now use `startAutoFocus` or the focus gesture ([#574][574])
- [*Camera2, Metering*] Improvement: `onAutoFocusEnd` is now guaranteed to be called ([#574][574]) - <small>[Camera2, Metering]</small> Improvement: `onAutoFocusEnd` is now guaranteed to be called ([#574][574])
- [*Camera2, Metering*] Improvement: taking picture does not invalidate the previous focus ([#574][574]) - <small>[Camera2, Metering]</small> Improvement: taking picture does not invalidate the previous focus ([#574][574])
- [*Camera2, Metering*] Improvement: better metering when zoomed in ([#574][574]) - <small>[Camera2, Metering]</small> Improvement: better metering when zoomed in ([#574][574])
- [*Real time filters*] **Breaking change**: `Filter` interface signatures now accept timestamps for animations ([#588][588]) - <small>[Real time filters]</small> **Breaking change**: `Filter` interface signatures now accept timestamps for animations ([#588][588])
- [*Overlays*] New: you can now use `addView()` and `removeView()` to add or remove overlays at runtime (see docs) ([#588][588]) - <small>[Overlays]</small> New: you can now use `addView()` and `removeView()` to add or remove overlays at runtime (see docs) ([#588][588])
- [*Video*] Improvement: better encoder selection ([#588][588]) - <small>[Video]</small> Improvement: better encoder selection ([#588][588])
- Fix: fixed various bugs and improved stability ([#588][588]) - Fix: fixed various bugs and improved stability ([#588][588])
https://github.com/natario1/CameraView/compare/v2.2.0...v2.3.0 https://github.com/natario1/CameraView/compare/v2.2.0...v2.3.0
## v2.2.0 ## v2.2.0
- [*Real time filters*] New: `SimpleFilter` class accepts a fragment shader in the constructor ([#552][552]) - <small>[Real time filters]</small> New: `SimpleFilter` class accepts a fragment shader in the constructor ([#552][552])
- [*Real time filters*] New: `MultiFilter` to apply more than one filter at the same time ([#559][559]) - <small>[Real time filters]</small> New: `MultiFilter` to apply more than one filter at the same time ([#559][559])
- [*Video*] Improvement: query device encoders before configuring them. Should fix issues on multiple devices ([#545][545]) - <small>[Video]</small> Improvement: query device encoders before configuring them. Should fix issues on multiple devices ([#545][545])
- [*Video*] Fix: `takeVideoSnapshot` not working unless you set a max duration ([#551][551]) - <small>[Video]</small> Fix: `takeVideoSnapshot` not working unless you set a max duration ([#551][551])
- [*Video*] Fix: `takeVideo` crashing on Camera2 LEGACY devices ([#551][551]) - <small>[Video]</small> Fix: `takeVideo` crashing on Camera2 LEGACY devices ([#551][551])
- [*Frame Processing*] Fix: fixed dead Frames issues and improved error messages ([#572][572]) - <small>[Frame Processing]</small> Fix: fixed dead Frames issues and improved error messages ([#572][572])
- Fix: fixed `CameraView` appearance in the layout editor ([#564][564]) - Fix: fixed `CameraView` appearance in the layout editor ([#564][564])
https://github.com/natario1/CameraView/compare/v2.1.0...v2.2.0 https://github.com/natario1/CameraView/compare/v2.1.0...v2.2.0

@ -39,11 +39,14 @@ device has cameras, and then start the camera view.
### Handling ### Handling
On Marshmallow+, the user must explicitly approve our permissions. You can On Marshmallow+, the user must explicitly approve our permissions. You can either:
- handle permissions yourself and then call `open()` or `setLifecycleOwner()` once they are acquired - handle permissions yourself and then call `open()` or `setLifecycleOwner()` once they are acquired
- ignore this: `CameraView` will present a permission request to the user based on - let `CameraView` request permissions: we will present a permission request to the user based on
whether they are needed or not with the current configuration. whether they are needed or not with the current configuration.
Note however, that this is done at the activity level, so the permission request callback The automatic request is currently done at the activity level, so the permission request callback
`onRequestPermissionResults()` will be invoked on the parent activity, not the fragment. `onRequestPermissionResults()` will be invoked on the parent activity, not the fragment.
The automatic request can be disabled by setting `app:cameraRequestPermissions="false"` in your
XML declaration.

@ -1,21 +1,29 @@
@import url('https://fonts.googleapis.com/css?family=Calistoga|Roboto+Mono|Source+Sans+Pro:400,700&display=swap');
body { body {
font-weight: 400; font-weight: 400;
font-family: 'Source Sans Pro', sans-serif;
}
h1, h2, h3, h4, h5, h6, th {
font-family: 'Calistoga', sans-serif;
}
small {
font-weight: 400;
font-family: 'Source Sans Pro', sans-serif;
} }
pre, code, pre code { pre, code, pre code {
border: none; border: none;
border-radius: 0; border-radius: 0;
background-color: #f9f9f9; background-color: #fafafa;
font-size: 1em; font-size: 0.8em;
} }
.highlight { .highlight {
background-color: #f9f9f9; background-color: #fafafa;
} border-radius: 12px;
/* This changes inline code and hopefully nothing else */
.highlighter-rouge {
color: #336699
} }
pre { pre {
@ -24,6 +32,12 @@ pre {
code { code {
color: inherit; color: inherit;
font-family: 'Roboto Mono', monospace;
color: #dd2200;
}
a {
color: #0e95e3;
} }
#header { #header {
@ -35,6 +49,27 @@ code {
text-decoration: none; text-decoration: none;
} }
#content {
/* bigger font */
font-size: 1.1em;
}
#content p {
/* spacing for all paragraphs */
margin-top: 16px;
margin-bottom: 16px;
}
#content ul li {
/* spacing for lists */
margin-top: 8px;
margin-bottom: 8px;
}
#content h5 {
font-size: 1.1em;
}
#footer { #footer {
margin: 20px 0; margin: 20px 0;
font-size: 0.85em; font-size: 0.85em;
@ -50,14 +85,27 @@ code {
margin-top: 0; margin-top: 0;
} }
#content .page-subtitle {
font-size: 0.45em;
}
#navigation { #navigation {
font-size: 0.9em; font-size: 1.0em;
} }
#navigation li a { #navigation li a {
padding-left: 10px; padding-left: 16px;
padding-right: 10px; padding-right: 16px;
border-radius: 32px;
}
#navigation li a:hover {
background: #fafafa;
}
#navigation li.active a {
background: #f76c16;
color: #FFFFFF;
} }
#navigation .nav-header { #navigation .nav-header {
@ -68,10 +116,11 @@ code {
} }
.nav-header { .nav-header {
font-size: 1em; font-size: 1.1em;
cursor: default; cursor: default;
text-transform: uppercase; /* text-transform: uppercase; */
font-weight: bold; font-weight: bold;
font-family: 'Calistoga', sans-serif;
} }
body.rtl { body.rtl {
@ -114,4 +163,5 @@ td {
th { th {
padding: 8px 12px; padding: 8px 12px;
border: 1px solid #e5e5e5; border: 1px solid #e5e5e5;
font-weight: 400;
} }

@ -6,7 +6,7 @@
.highlight .cm { color: #888888 } /* Comment.Multiline */ .highlight .cm { color: #888888 } /* Comment.Multiline */
.highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */ .highlight .cp { color: #cc0000; font-weight: bold } /* Comment.Preproc */
.highlight .c1 { color: #888888 } /* Comment.Single */ .highlight .c1 { color: #888888 } /* Comment.Single */
.highlight .cs { color: #cc0000; font-weight: bold; background-color: #fff0f0 } /* Comment.Special */ .highlight .cs { color: #cc0000; font-weight: bold; /* background-color: #fff0f0 */ } /* Comment.Special */
.highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */ .highlight .gd { color: #000000; background-color: #ffdddd } /* Generic.Deleted */
.highlight .ge { font-style: italic } /* Generic.Emph */ .highlight .ge { font-style: italic } /* Generic.Emph */
.highlight .gr { color: #aa0000 } /* Generic.Error */ .highlight .gr { color: #aa0000 } /* Generic.Error */

@ -1,6 +1,6 @@
--- ---
layout: default layout: default
title: "CameraView v2" title: "CameraView"
--- ---
# CameraView # CameraView

Loading…
Cancel
Save