1 /* 2 * Copyright 2024 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package androidx.camera.integration.view 18 19 import android.annotation.SuppressLint 20 import android.content.ContentValues 21 import android.os.Build 22 import android.os.Bundle 23 import android.provider.MediaStore 24 import android.view.LayoutInflater 25 import android.view.View 26 import android.view.ViewGroup 27 import android.widget.Button 28 import android.widget.SeekBar 29 import android.widget.Toast 30 import android.widget.ToggleButton 31 import androidx.annotation.OptIn 32 import androidx.annotation.RequiresApi 33 import androidx.camera.core.CameraEffect.IMAGE_CAPTURE 34 import androidx.camera.core.CameraEffect.PREVIEW 35 import androidx.camera.core.CameraEffect.VIDEO_CAPTURE 36 import androidx.camera.core.CameraSelector 37 import androidx.camera.core.DynamicRange 38 import androidx.camera.core.impl.utils.executor.CameraXExecutors.directExecutor 39 import androidx.camera.core.impl.utils.executor.CameraXExecutors.mainThreadExecutor 40 import androidx.camera.media3.effect.Media3Effect 41 import androidx.camera.video.MediaStoreOutputOptions 42 import androidx.camera.video.Recording 43 import androidx.camera.video.VideoRecordEvent 44 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_DURATION_LIMIT_REACHED 45 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_FILE_SIZE_LIMIT_REACHED 46 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_INSUFFICIENT_STORAGE 47 import androidx.camera.video.VideoRecordEvent.Finalize.ERROR_SOURCE_INACTIVE 48 import androidx.camera.view.CameraController 49 import androidx.camera.view.LifecycleCameraController 50 import androidx.camera.view.PreviewView 51 import androidx.camera.view.video.AudioConfig 52 import androidx.fragment.app.Fragment 53 import androidx.media3.common.util.UnstableApi 54 import androidx.media3.effect.Brightness 55 56 /** Fragment for testing effects integration. */ 57 @OptIn(UnstableApi::class) 58 @Suppress("RestrictedApiAndroidX") 59 class Media3EffectsFragment : Fragment() { 60 61 private lateinit var media3Effect: Media3Effect 62 lateinit var cameraController: LifecycleCameraController 63 lateinit var previewView: PreviewView 64 lateinit var slider: SeekBar 65 private lateinit var hdrToggle: ToggleButton 66 private lateinit var cameraToggle: ToggleButton 67 private lateinit var recordButton: Button 68 private var recording: Recording? = null 69 70 @RequiresApi(Build.VERSION_CODES.O) onCreateViewnull71 override fun onCreateView( 72 inflater: LayoutInflater, 73 container: ViewGroup?, 74 savedInstanceState: Bundle? 75 ): View? { 76 // Inflate the layout for this fragment. 77 val view = inflater.inflate(R.layout.media3_effect_view, container, false) 78 previewView = view.findViewById(R.id.preview_view) 79 slider = view.findViewById(R.id.slider) 80 hdrToggle = view.findViewById(R.id.hdr) 81 cameraToggle = view.findViewById(R.id.flip) 82 recordButton = view.findViewById(R.id.record) 83 84 cameraController = LifecycleCameraController(requireContext()) 85 cameraController.bindToLifecycle(viewLifecycleOwner) 86 cameraController.setEnabledUseCases(CameraController.VIDEO_CAPTURE) 87 previewView.controller = cameraController 88 89 slider.setOnSeekBarChangeListener( 90 object : SeekBar.OnSeekBarChangeListener { 91 override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) { 92 media3Effect.setEffects(listOf(Brightness(progress / 100f))) 93 } 94 95 override fun onStartTrackingTouch(seekBar: SeekBar) = Unit 96 97 override fun onStopTrackingTouch(seekBar: SeekBar) = Unit 98 } 99 ) 100 cameraToggle.setOnClickListener { updateCameraOrientation() } 101 hdrToggle.setOnClickListener { 102 cameraController.videoCaptureDynamicRange = 103 if (hdrToggle.isChecked) { 104 DynamicRange.HDR_UNSPECIFIED_10_BIT 105 } else { 106 DynamicRange.SDR 107 } 108 } 109 recordButton.setOnClickListener { 110 if (recording == null) { 111 startRecording() 112 } else { 113 stopRecording() 114 } 115 } 116 media3Effect = 117 Media3Effect( 118 requireContext(), 119 PREVIEW or VIDEO_CAPTURE or IMAGE_CAPTURE, 120 mainThreadExecutor(), 121 ) { 122 toast("Error in CameraFiltersAdapterProcessor") 123 } 124 cameraController.setEffects(setOf(media3Effect)) 125 // Set up UI events. 126 return view 127 } 128 updateCameraOrientationnull129 private fun updateCameraOrientation() { 130 cameraController.cameraSelector = 131 if (cameraToggle.isChecked) { 132 CameraSelector.DEFAULT_FRONT_CAMERA 133 } else { 134 CameraSelector.DEFAULT_BACK_CAMERA 135 } 136 } 137 onDestroyViewnull138 override fun onDestroyView() { 139 super.onDestroyView() 140 media3Effect.close() 141 } 142 toastnull143 private fun toast(message: String?) { 144 requireActivity().runOnUiThread { 145 Toast.makeText(context, message, Toast.LENGTH_SHORT).show() 146 } 147 } 148 149 @SuppressLint("MissingPermission") startRecordingnull150 private fun startRecording() { 151 recordButton.text = "Stop recording" 152 val outputOptions: MediaStoreOutputOptions = getNewVideoOutputMediaStoreOptions() 153 val audioConfig = AudioConfig.create(true) 154 recording = 155 cameraController.startRecording(outputOptions, audioConfig, directExecutor()) { 156 if (it is VideoRecordEvent.Finalize) { 157 val uri = it.outputResults.outputUri 158 when (it.error) { 159 VideoRecordEvent.Finalize.ERROR_NONE, 160 ERROR_FILE_SIZE_LIMIT_REACHED, 161 ERROR_DURATION_LIMIT_REACHED, 162 ERROR_INSUFFICIENT_STORAGE, 163 ERROR_SOURCE_INACTIVE -> toast("Video saved to: $uri") 164 else -> toast("Failed to save video: uri $uri with code (${it.error})") 165 } 166 } 167 } 168 } 169 stopRecordingnull170 private fun stopRecording() { 171 recordButton.text = "Record" 172 recording?.stop() 173 recording = null 174 } 175 getNewVideoOutputMediaStoreOptionsnull176 private fun getNewVideoOutputMediaStoreOptions(): MediaStoreOutputOptions { 177 val videoFileName = "video_" + System.currentTimeMillis() 178 val resolver = requireContext().contentResolver 179 val contentValues = ContentValues() 180 contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "video/mp4") 181 contentValues.put(MediaStore.Video.Media.TITLE, videoFileName) 182 contentValues.put(MediaStore.Video.Media.DISPLAY_NAME, videoFileName) 183 return MediaStoreOutputOptions.Builder( 184 resolver, 185 MediaStore.Video.Media.EXTERNAL_CONTENT_URI 186 ) 187 .setContentValues(contentValues) 188 .build() 189 } 190 } 191