From de0d6cda5d85e07482b3c274dc0ee2e709efddbc Mon Sep 17 00:00:00 2001 From: Marc Rousavy Date: Tue, 24 Oct 2023 11:19:03 +0200 Subject: [PATCH] feat: Implement atomically single-lock `core/` library on Android (#2049) * feat: Create base for `CameraConfiguration` diff * Fix * Write three configure methods * Build? * MOre * Update CameraView+RecordVideo.kt * Fix errors * Update CameraDeviceDetails.kt * Update CameraSession.kt * Auto-resize Preview View * More * Make it work? idk * Format * Call `configure` under mutex, and change isActive * fix: Make Outputs comparable * fix: Make CodeScanner comparable * Format * fix: Update outputs after reconfiguring * Update CameraPage.tsx * fix: Close CaptureSession before --- .../mrousavy/camera/CameraView+RecordVideo.kt | 13 +- .../mrousavy/camera/CameraView+TakePhoto.kt | 2 +- .../java/com/mrousavy/camera/CameraView.kt | 259 +++---- .../com/mrousavy/camera/CameraViewManager.kt | 78 +- .../camera/core/CameraConfiguration.kt | 103 +++ .../camera/core/CameraDeviceDetails.kt | 4 +- .../com/mrousavy/camera/core/CameraError.kt | 7 +- .../com/mrousavy/camera/core/CameraSession.kt | 732 +++++++++--------- .../camera/core/CodeScannerPipeline.kt | 16 +- .../com/mrousavy/camera/core/PreviewView.kt | 110 +-- .../com/mrousavy/camera/core/VideoPipeline.kt | 13 +- .../camera/core/outputs/CameraOutputs.kt | 177 ----- .../{ImageReaderOutput.kt => PhotoOutput.kt} | 6 +- .../camera/core/outputs/SurfaceOutput.kt | 11 +- .../core/outputs/VideoPipelineOutput.kt | 6 +- .../CameraCharacteristics+getPreviewSize.kt | 2 +- .../CameraDevice+createCaptureSession.kt | 33 +- .../mrousavy/camera/types/AutoFocusSystem.kt | 14 + .../camera/types/CameraDeviceFormat.kt | 106 +++ .../com/mrousavy/camera/types/Orientation.kt | 4 +- .../com/mrousavy/camera/types/PixelFormat.kt | 5 +- .../camera/types/VideoStabilizationMode.kt | 5 +- package/example/ios/Podfile.lock | 4 +- 23 files changed, 821 insertions(+), 889 deletions(-) create mode 100644 package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt delete mode 100644 package/android/src/main/java/com/mrousavy/camera/core/outputs/CameraOutputs.kt rename package/android/src/main/java/com/mrousavy/camera/core/outputs/{ImageReaderOutput.kt => PhotoOutput.kt} (77%) create mode 100644 package/android/src/main/java/com/mrousavy/camera/types/AutoFocusSystem.kt create mode 100644 package/android/src/main/java/com/mrousavy/camera/types/CameraDeviceFormat.kt diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt index 04128d1..6dc3ab5 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+RecordVideo.kt @@ -23,10 +23,12 @@ suspend fun CameraView.startRecording(options: ReadableMap, onRecordCallback: Ca } } - if (options.hasKey("flash")) { - val enableFlash = options.getString("flash") == "on" + val enableFlash = options.getString("flash") == "on" + if (enableFlash) { // overrides current torch mode value to enable flash while recording - cameraSession.setTorchMode(enableFlash) + cameraSession.configure { config -> + config.torch = Torch.ON + } } var codec = VideoCodec.H264 if (options.hasKey("videoCodec")) { @@ -67,5 +69,8 @@ suspend fun CameraView.resumeRecording() { @SuppressLint("RestrictedApi") suspend fun CameraView.stopRecording() { cameraSession.stopRecording() - cameraSession.setTorchMode(torch == Torch.ON) + // Set torch back to it's original value in case we just used it as a flash for the recording. + cameraSession.configure { config -> + config.torch = torch + } } diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt index 562b0a7..88c085f 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView+TakePhoto.kt @@ -41,7 +41,7 @@ suspend fun CameraView.takePhoto(optionsMap: ReadableMap): WritableMap { enableShutterSound, enableAutoRedEyeReduction, enableAutoStabilization, - outputOrientation + orientation ) photo.use { diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt index 42b709e..4d2e1d0 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraView.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraView.kt @@ -1,31 +1,20 @@ package com.mrousavy.camera -import android.Manifest import android.annotation.SuppressLint import android.content.Context -import android.content.pm.PackageManager import android.hardware.camera2.CameraManager import android.util.Log -import android.util.Size -import android.view.Gravity import android.view.ScaleGestureDetector -import android.view.Surface import android.widget.FrameLayout -import androidx.core.content.ContextCompat import com.facebook.react.bridge.ReadableMap -import com.facebook.react.bridge.UiThreadUtil -import com.mrousavy.camera.core.CameraPermissionError +import com.google.mlkit.vision.barcode.common.Barcode +import com.mrousavy.camera.core.CameraConfiguration import com.mrousavy.camera.core.CameraQueues import com.mrousavy.camera.core.CameraSession -import com.mrousavy.camera.core.NoCameraDeviceError import com.mrousavy.camera.core.PreviewView -import com.mrousavy.camera.core.outputs.CameraOutputs -import com.mrousavy.camera.extensions.bigger -import com.mrousavy.camera.extensions.containsAny -import com.mrousavy.camera.extensions.getPreviewTargetSize import com.mrousavy.camera.extensions.installHierarchyFitter -import com.mrousavy.camera.extensions.smaller import com.mrousavy.camera.frameprocessor.FrameProcessor +import com.mrousavy.camera.types.CameraDeviceFormat import com.mrousavy.camera.types.CodeScannerOptions import com.mrousavy.camera.types.Orientation import com.mrousavy.camera.types.PixelFormat @@ -48,19 +37,23 @@ import kotlinx.coroutines.launch @SuppressLint("ClickableViewAccessibility", "ViewConstructor", "MissingPermission") class CameraView(context: Context) : FrameLayout(context), - CoroutineScope { + CoroutineScope, + CameraSession.CameraSessionCallback { companion object { const val TAG = "CameraView" - - private val propsThatRequirePreviewReconfiguration = arrayListOf("cameraId", "format", "resizeMode") - private val propsThatRequireSessionReconfiguration = - arrayListOf("cameraId", "format", "photo", "video", "enableFrameProcessor", "codeScannerOptions", "pixelFormat") - private val propsThatRequireFormatReconfiguration = arrayListOf("fps", "hdr", "videoStabilizationMode", "lowLightBoost") } // react properties // props that require reconfiguring var cameraId: String? = null + set(value) { + if (value != null) { + // TODO: Move this into CameraSession + val f = if (format != null) CameraDeviceFormat.fromJSValue(format!!) else null + previewView.resizeToInputCamera(value, cameraManager, f) + } + field = value + } var enableDepthData = false var enableHighQualityPhotos: Boolean? = null var enablePortraitEffectsMatteDelivery = false @@ -74,7 +67,6 @@ class CameraView(context: Context) : // props that require format reconfiguring var format: ReadableMap? = null - var resizeMode: ResizeMode = ResizeMode.COVER var fps: Int? = null var videoStabilizationMode: VideoStabilizationMode? = null var hdr: Boolean? = null // nullable bool @@ -84,8 +76,17 @@ class CameraView(context: Context) : var isActive = false var torch: Torch = Torch.OFF var zoom: Float = 1f // in "factor" - var orientation: Orientation? = null + var orientation: Orientation = Orientation.PORTRAIT var enableZoomGesture: Boolean = false + set(value) { + field = value + updateZoomGesture() + } + var resizeMode: ResizeMode = ResizeMode.COVER + set(value) { + previewView.resizeMode = value + field = value + } // code scanner var codeScannerOptions: CodeScannerOptions? = null @@ -96,8 +97,7 @@ class CameraView(context: Context) : // session internal val cameraSession: CameraSession - private var previewView: PreviewView? = null - private var previewSurface: Surface? = null + private val previewView: PreviewView internal var frameProcessor: FrameProcessor? = null set(value) { @@ -105,167 +105,100 @@ class CameraView(context: Context) : cameraSession.frameProcessor = frameProcessor } - private val inputOrientation: Orientation - get() = cameraSession.orientation - internal val outputOrientation: Orientation - get() = orientation ?: inputOrientation - override val coroutineContext: CoroutineContext = CameraQueues.cameraQueue.coroutineDispatcher init { this.installHierarchyFitter() clipToOutline = true - setupPreviewView() - cameraSession = CameraSession(context, cameraManager, { invokeOnInitialized() }, { error -> invokeOnError(error) }) + cameraSession = CameraSession(context, cameraManager, this) + previewView = cameraSession.createPreviewView(context) + addView(previewView) } override fun onAttachedToWindow() { - super.onAttachedToWindow() if (!isMounted) { isMounted = true invokeOnViewReady() } - launch { updateLifecycle() } + update() + super.onAttachedToWindow() } override fun onDetachedFromWindow() { + update() super.onDetachedFromWindow() - launch { updateLifecycle() } } - private fun getPreviewTargetSize(): Size { - val cameraId = cameraId ?: throw NoCameraDeviceError() - - val format = format - val targetPreviewSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null - val formatAspectRatio = if (targetPreviewSize != null) targetPreviewSize.bigger.toDouble() / targetPreviewSize.smaller else null - - return this.cameraManager.getCameraCharacteristics(cameraId).getPreviewTargetSize(formatAspectRatio) - } - - private fun setupPreviewView() { - removeView(previewView) - this.previewSurface = null - - if (cameraId == null) return - - val previewView = PreviewView(context, this.getPreviewTargetSize(), resizeMode) { surface -> - previewSurface = surface - launch { configureSession() } - } - previewView.layoutParams = LayoutParams( - LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT, - Gravity.CENTER - ) - this.previewView = previewView - UiThreadUtil.runOnUiThread { - addView(previewView) - } - } - - fun update(changedProps: ArrayList) { - Log.i(TAG, "Props changed: $changedProps") - val shouldReconfigurePreview = changedProps.containsAny(propsThatRequirePreviewReconfiguration) - val shouldReconfigureSession = shouldReconfigurePreview || changedProps.containsAny(propsThatRequireSessionReconfiguration) - val shouldReconfigureFormat = shouldReconfigureSession || changedProps.containsAny(propsThatRequireFormatReconfiguration) - val shouldReconfigureZoom = shouldReconfigureSession || changedProps.contains("zoom") - val shouldReconfigureTorch = shouldReconfigureSession || changedProps.contains("torch") - val shouldCheckActive = shouldReconfigureFormat || changedProps.contains("isActive") - val shouldReconfigureZoomGesture = changedProps.contains("enableZoomGesture") + fun update() { + Log.i(TAG, "Updating CameraSession...") launch { - try { - // Expensive Calls - if (shouldReconfigurePreview) { - setupPreviewView() + cameraSession.configure { config -> + // Input Camera Device + config.cameraId = cameraId + + // Photo + if (photo == true) { + config.photo = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Photo(Unit)) + } else { + config.photo = CameraConfiguration.Output.Disabled.create() } - if (shouldReconfigureSession) { - configureSession() + + // Video/Frame Processor + if (video == true || enableFrameProcessor) { + config.video = CameraConfiguration.Output.Enabled.create( + CameraConfiguration.Video( + pixelFormat, + enableFrameProcessor + ) + ) + } else { + config.video = CameraConfiguration.Output.Disabled.create() } - if (shouldReconfigureFormat) { - configureFormat() + + // Audio + if (audio == true) { + config.audio = CameraConfiguration.Output.Enabled.create(CameraConfiguration.Audio(Unit)) + } else { + config.audio = CameraConfiguration.Output.Disabled.create() } - if (shouldCheckActive) { - updateLifecycle() + + // Code Scanner + val codeScanner = codeScannerOptions + if (codeScanner != null) { + config.codeScanner = CameraConfiguration.Output.Enabled.create( + CameraConfiguration.CodeScanner(codeScanner.codeTypes) + ) + } else { + config.codeScanner = CameraConfiguration.Output.Disabled.create() } - // Fast Calls - if (shouldReconfigureZoom) { - updateZoom() + + // Orientation + config.orientation = orientation + + // Format + val format = format + if (format != null) { + config.format = CameraDeviceFormat.fromJSValue(format) + } else { + config.format = null } - if (shouldReconfigureTorch) { - updateTorch() - } - if (shouldReconfigureZoomGesture) { - updateZoomGesture() - } - } catch (e: Throwable) { - Log.e(TAG, "update() threw: ${e.message}") - invokeOnError(e) + + // Side-Props + config.fps = fps + config.enableLowLightBoost = lowLightBoost ?: false + config.enableHdr = hdr ?: false + config.torch = torch + + // Zoom + config.zoom = zoom + + // isActive + config.isActive = isActive && isAttachedToWindow } } } - private suspend fun configureSession() { - try { - Log.i(TAG, "Configuring Camera Device...") - - if (ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { - throw CameraPermissionError() - } - val cameraId = cameraId ?: throw NoCameraDeviceError() - - val format = format - val targetVideoSize = if (format != null) Size(format.getInt("videoWidth"), format.getInt("videoHeight")) else null - val targetPhotoSize = if (format != null) Size(format.getInt("photoWidth"), format.getInt("photoHeight")) else null - // TODO: Allow previewSurface to be null/none - val previewSurface = previewSurface ?: return - val codeScannerOptions = codeScannerOptions - - val previewOutput = CameraOutputs.PreviewOutput(previewSurface, previewView?.targetSize) - val photoOutput = if (photo == true) { - CameraOutputs.PhotoOutput(targetPhotoSize) - } else { - null - } - val videoOutput = if (video == true || enableFrameProcessor) { - CameraOutputs.VideoOutput(targetVideoSize, video == true, enableFrameProcessor, pixelFormat) - } else { - null - } - val codeScanner = if (codeScannerOptions != null) { - CameraOutputs.CodeScannerOutput( - codeScannerOptions, - { codes -> invokeOnCodeScanned(codes) }, - { error -> invokeOnError(error) } - ) - } else { - null - } - - cameraSession.configureSession(cameraId, previewOutput, photoOutput, videoOutput, codeScanner) - } catch (e: Throwable) { - Log.e(TAG, "Failed to configure session: ${e.message}", e) - invokeOnError(e) - } - } - - private suspend fun configureFormat() { - cameraSession.configureFormat(fps, videoStabilizationMode, hdr, lowLightBoost) - } - - private suspend fun updateLifecycle() { - cameraSession.setIsActive(isActive && isAttachedToWindow) - } - - private suspend fun updateZoom() { - cameraSession.setZoom(zoom) - } - - private suspend fun updateTorch() { - cameraSession.setTorchMode(torch == Torch.ON) - } - @SuppressLint("ClickableViewAccessibility") private fun updateZoomGesture() { if (enableZoomGesture) { @@ -274,7 +207,7 @@ class CameraView(context: Context) : object : ScaleGestureDetector.SimpleOnScaleGestureListener() { override fun onScale(detector: ScaleGestureDetector): Boolean { zoom *= detector.scaleFactor - launch { updateZoom() } + update() return true } } @@ -286,4 +219,16 @@ class CameraView(context: Context) : setOnTouchListener(null) } } + + override fun onError(error: Throwable) { + invokeOnError(error) + } + + override fun onInitialized() { + invokeOnInitialized() + } + + override fun onCodeScanned(codes: List) { + invokeOnCodeScanned(codes) + } } diff --git a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt index 599562c..0d25383 100644 --- a/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt +++ b/package/android/src/main/java/com/mrousavy/camera/CameraViewManager.kt @@ -18,9 +18,7 @@ class CameraViewManager : ViewGroupManager() { override fun onAfterUpdateTransaction(view: CameraView) { super.onAfterUpdateTransaction(view) - val changedProps = cameraViewTransactions[view] ?: ArrayList() - view.update(changedProps) - cameraViewTransactions.remove(view) + view.update() } override fun getExportedCustomDirectEventTypeConstants(): MutableMap? = @@ -35,108 +33,69 @@ class CameraViewManager : ViewGroupManager() { @ReactProp(name = "cameraId") fun setCameraId(view: CameraView, cameraId: String) { - if (view.cameraId != cameraId) { - addChangedPropToTransaction(view, "cameraId") - } view.cameraId = cameraId } @ReactProp(name = "photo") fun setPhoto(view: CameraView, photo: Boolean?) { - if (view.photo != photo) { - addChangedPropToTransaction(view, "photo") - } view.photo = photo } @ReactProp(name = "video") fun setVideo(view: CameraView, video: Boolean?) { - if (view.video != video) { - addChangedPropToTransaction(view, "video") - } view.video = video } @ReactProp(name = "audio") fun setAudio(view: CameraView, audio: Boolean?) { - if (view.audio != audio) { - addChangedPropToTransaction(view, "audio") - } view.audio = audio } @ReactProp(name = "enableFrameProcessor") fun setEnableFrameProcessor(view: CameraView, enableFrameProcessor: Boolean) { - if (view.enableFrameProcessor != enableFrameProcessor) { - addChangedPropToTransaction(view, "enableFrameProcessor") - } view.enableFrameProcessor = enableFrameProcessor } @ReactProp(name = "pixelFormat") fun setPixelFormat(view: CameraView, pixelFormat: String?) { val newPixelFormat = PixelFormat.fromUnionValue(pixelFormat) - if (view.pixelFormat != newPixelFormat) { - addChangedPropToTransaction(view, "pixelFormat") - } - view.pixelFormat = newPixelFormat ?: PixelFormat.NATIVE + view.pixelFormat = newPixelFormat } @ReactProp(name = "enableDepthData") fun setEnableDepthData(view: CameraView, enableDepthData: Boolean) { - if (view.enableDepthData != enableDepthData) { - addChangedPropToTransaction(view, "enableDepthData") - } view.enableDepthData = enableDepthData } @ReactProp(name = "enableZoomGesture") fun setEnableZoomGesture(view: CameraView, enableZoomGesture: Boolean) { - if (view.enableZoomGesture != enableZoomGesture) { - addChangedPropToTransaction(view, "enableZoomGesture") - } view.enableZoomGesture = enableZoomGesture } @ReactProp(name = "videoStabilizationMode") fun setVideoStabilizationMode(view: CameraView, videoStabilizationMode: String?) { val newMode = VideoStabilizationMode.fromUnionValue(videoStabilizationMode) - if (view.videoStabilizationMode != newMode) { - addChangedPropToTransaction(view, "videoStabilizationMode") - } view.videoStabilizationMode = newMode } @ReactProp(name = "enableHighQualityPhotos") fun setEnableHighQualityPhotos(view: CameraView, enableHighQualityPhotos: Boolean?) { - if (view.enableHighQualityPhotos != enableHighQualityPhotos) { - addChangedPropToTransaction(view, "enableHighQualityPhotos") - } view.enableHighQualityPhotos = enableHighQualityPhotos } @ReactProp(name = "enablePortraitEffectsMatteDelivery") fun setEnablePortraitEffectsMatteDelivery(view: CameraView, enablePortraitEffectsMatteDelivery: Boolean) { - if (view.enablePortraitEffectsMatteDelivery != enablePortraitEffectsMatteDelivery) { - addChangedPropToTransaction(view, "enablePortraitEffectsMatteDelivery") - } view.enablePortraitEffectsMatteDelivery = enablePortraitEffectsMatteDelivery } @ReactProp(name = "format") fun setFormat(view: CameraView, format: ReadableMap?) { - if (view.format != format) { - addChangedPropToTransaction(view, "format") - } view.format = format } @ReactProp(name = "resizeMode") fun setResizeMode(view: CameraView, resizeMode: String) { val newMode = ResizeMode.fromUnionValue(resizeMode) - if (view.resizeMode != newMode) { - addChangedPropToTransaction(view, "resizeMode") - } view.resizeMode = newMode } @@ -145,82 +104,49 @@ class CameraViewManager : ViewGroupManager() { // of type "Int?" the react bridge throws an error. @ReactProp(name = "fps", defaultInt = -1) fun setFps(view: CameraView, fps: Int) { - if (view.fps != fps) { - addChangedPropToTransaction(view, "fps") - } view.fps = if (fps > 0) fps else null } @ReactProp(name = "hdr") fun setHdr(view: CameraView, hdr: Boolean?) { - if (view.hdr != hdr) { - addChangedPropToTransaction(view, "hdr") - } view.hdr = hdr } @ReactProp(name = "lowLightBoost") fun setLowLightBoost(view: CameraView, lowLightBoost: Boolean?) { - if (view.lowLightBoost != lowLightBoost) { - addChangedPropToTransaction(view, "lowLightBoost") - } view.lowLightBoost = lowLightBoost } @ReactProp(name = "isActive") fun setIsActive(view: CameraView, isActive: Boolean) { - if (view.isActive != isActive) { - addChangedPropToTransaction(view, "isActive") - } view.isActive = isActive } @ReactProp(name = "torch") fun setTorch(view: CameraView, torch: String) { val newMode = Torch.fromUnionValue(torch) - if (view.torch != newMode) { - addChangedPropToTransaction(view, "torch") - } view.torch = newMode } @ReactProp(name = "zoom") fun setZoom(view: CameraView, zoom: Double) { val zoomFloat = zoom.toFloat() - if (view.zoom != zoomFloat) { - addChangedPropToTransaction(view, "zoom") - } view.zoom = zoomFloat } @ReactProp(name = "orientation") fun setOrientation(view: CameraView, orientation: String?) { val newMode = Orientation.fromUnionValue(orientation) - if (view.orientation != newMode) { - addChangedPropToTransaction(view, "orientation") - } view.orientation = newMode } @ReactProp(name = "codeScannerOptions") fun setCodeScanner(view: CameraView, codeScannerOptions: ReadableMap) { val newCodeScannerOptions = CodeScannerOptions(codeScannerOptions) - if (view.codeScannerOptions != newCodeScannerOptions) { - addChangedPropToTransaction(view, "codeScannerOptions") - } view.codeScannerOptions = newCodeScannerOptions } companion object { const val TAG = "CameraView" - - val cameraViewTransactions: HashMap> = HashMap() - - private fun addChangedPropToTransaction(view: CameraView, changedProp: String) { - if (cameraViewTransactions[view] == null) { - cameraViewTransactions[view] = ArrayList() - } - cameraViewTransactions[view]!!.add(changedProp) - } } } diff --git a/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt new file mode 100644 index 0000000..b13115a --- /dev/null +++ b/package/android/src/main/java/com/mrousavy/camera/core/CameraConfiguration.kt @@ -0,0 +1,103 @@ +package com.mrousavy.camera.core + +import android.view.Surface +import com.mrousavy.camera.types.CameraDeviceFormat +import com.mrousavy.camera.types.CodeType +import com.mrousavy.camera.types.Orientation +import com.mrousavy.camera.types.PixelFormat +import com.mrousavy.camera.types.Torch +import com.mrousavy.camera.types.VideoStabilizationMode + +data class CameraConfiguration( + // Input + var cameraId: String? = null, + + // Outputs + var preview: Output = Output.Disabled.create(), + var photo: Output = Output.Disabled.create(), + var video: Output