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