Last active
March 17, 2023 12:28
-
-
Save ThomasGorisse/4840069d43c14b845791912c34cf44a5 to your computer and use it in GitHub Desktop.
SceneView Android screenshot and video screen recorder
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.gorisse.thomas.arcamera | |
import android.Manifest | |
import android.content.ContentValues | |
import android.content.Context | |
import android.content.Intent | |
import android.content.res.Configuration | |
import android.graphics.Bitmap | |
import android.hardware.display.DisplayManager | |
import android.media.CamcorderProfile | |
import android.media.MediaRecorder | |
import android.net.Uri | |
import android.os.* | |
import android.provider.MediaStore | |
import android.view.PixelCopy | |
import android.view.Surface | |
import androidx.lifecycle.lifecycleScope | |
import com.touticom.ticfoundation.Intents | |
import com.touticom.ticfoundation.string | |
import com.touticom.ticfoundation.verifyPermission | |
import kotlinx.coroutines.Dispatchers | |
import kotlinx.coroutines.withContext | |
import java.text.SimpleDateFormat | |
import java.util.* | |
import kotlin.math.max | |
import kotlin.math.min | |
object SceneViewRecorder { | |
private var mediaRecorder: MediaRecorder? = null | |
var recordFileDescriptor: ParcelFileDescriptor? = null | |
var recordUri: Uri? = null | |
fun takePhoto(fragment: Fragment) { | |
fragment.lifecycleScope.launchWhenCreated { | |
withContext(Dispatchers.IO) { | |
val context = fragment.requireContext() | |
val fileName = "${context.string(R.string.app_name)}-${ | |
SimpleDateFormat( | |
"yyyyMMdd_HHmmss", | |
Locale.US | |
).format(Date()) | |
}.jpg" | |
val values = ContentValues().apply { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | |
put( | |
MediaStore.Images.Media.RELATIVE_PATH, | |
"${Environment.DIRECTORY_DCIM}/ARCamera" | |
) | |
} | |
put(MediaStore.Images.Media.DISPLAY_NAME, fileName) | |
put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg") | |
} | |
val photoUri = fragment.requireContext().contentResolver.insert( | |
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, | |
values | |
)!! | |
val photoFileDescriptor = | |
context.contentResolver.openFileDescriptor(photoUri, "w")!! | |
val bitmap = Bitmap.createBitmap( | |
fragment.binding.sceneView.width, | |
fragment.binding.sceneView.height, | |
Bitmap.Config.ARGB_8888 | |
) | |
PixelCopy.request( | |
fragment.binding.sceneView, bitmap, { result -> | |
if (result == PixelCopy.SUCCESS) { | |
fragment.lifecycleScope.launchWhenCreated { | |
withContext(Dispatchers.IO) { | |
bitmap.compress( | |
Bitmap.CompressFormat.PNG, | |
100, | |
context.contentResolver.openOutputStream(photoUri) | |
) | |
} | |
withContext(Dispatchers.Main) { | |
fragment.setLoading(false) | |
fragment.isRecording = false | |
try { | |
fragment.activity.startActivity( | |
Intent(Intent.ACTION_SEND) | |
.setType("image/jpeg") | |
.putExtra(Intent.EXTRA_STREAM, photoUri) | |
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) | |
.apply { | |
if (BuildConfig.SHARE_PACKAGE.isNotEmpty()) { | |
setPackage(BuildConfig.SHARE_PACKAGE) | |
} | |
} | |
) | |
} catch (e: Exception) { | |
Intents.view( | |
fragment.activity, | |
url = "https://play.google.com/store/apps/details?id=${BuildConfig.SHARE_PACKAGE}" | |
) | |
} | |
} | |
} | |
} | |
}, Handler(Looper.getMainLooper()) | |
) | |
} | |
} | |
} | |
fun startVideoRecord(fragment: Fragment) { | |
verifyPermission(activity = fragment.activity, | |
permissions = listOf( | |
Manifest.permission.RECORD_AUDIO | |
), | |
fallBack = { | |
fragment.isRecording = false | |
}, | |
success = { | |
val context = fragment.requireContext() | |
val fileName = "${context.string(R.string.app_name)}-${ | |
SimpleDateFormat( | |
"yyyyMMdd_HHmmss", | |
Locale.US | |
).format(Date()) | |
}.mp4" | |
val values = ContentValues().apply { | |
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { | |
put( | |
MediaStore.Video.Media.RELATIVE_PATH, | |
"${Environment.DIRECTORY_MOVIES}/ARCamera" | |
) | |
} | |
put(MediaStore.Video.Media.DISPLAY_NAME, fileName) | |
put(MediaStore.Video.Media.MIME_TYPE, "video/mp4") | |
put(MediaStore.Video.Media.IS_PENDING, 1) | |
} | |
recordUri = context.contentResolver.insert( | |
MediaStore.Video.Media.EXTERNAL_CONTENT_URI, | |
values | |
)!! | |
recordFileDescriptor = | |
context.contentResolver.openFileDescriptor(recordUri!!, "w")!! | |
val supportedVideoQuality = | |
(fragment.videoQuality downTo 0).firstOrNull { CamcorderProfile.hasProfile(it) } | |
if (supportedVideoQuality != null) { | |
val profile = CamcorderProfile.get(supportedVideoQuality) | |
var (width, height) = 0 to 0 | |
val isLandscape = | |
context.resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE | |
if (isLandscape) { | |
width = min( | |
max(profile.videoFrameWidth, profile.videoFrameHeight), | |
max(fragment.binding.sceneView.width, fragment.binding.sceneView.height) | |
) | |
height = min( | |
min(profile.videoFrameWidth, profile.videoFrameHeight), | |
min(fragment.binding.sceneView.width, fragment.binding.sceneView.height) | |
) | |
} else { | |
width = min( | |
min(profile.videoFrameWidth, profile.videoFrameHeight), | |
min(fragment.binding.sceneView.width, fragment.binding.sceneView.height) | |
) | |
height = min( | |
max(profile.videoFrameWidth, profile.videoFrameHeight), | |
max(fragment.binding.sceneView.width, fragment.binding.sceneView.height) | |
) | |
} | |
mediaRecorder = MediaRecorder().apply { | |
val displayManager: DisplayManager = | |
context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager | |
val rotation = when (displayManager.displays[0].rotation) { | |
Surface.ROTATION_90 -> 270 | |
Surface.ROTATION_180 -> 180 | |
Surface.ROTATION_270 -> 90 | |
else -> 0 | |
} | |
setOrientationHint(rotation) | |
setAudioSource(MediaRecorder.AudioSource.MIC) | |
setVideoSource(MediaRecorder.VideoSource.SURFACE) | |
setOutputFormat(MediaRecorder.OutputFormat.MPEG_4) | |
setOutputFile(recordFileDescriptor!!.fileDescriptor) | |
setVideoEncodingBitRate(profile.videoBitRate) | |
setVideoFrameRate(profile.videoFrameRate) | |
setVideoSize(width, height) | |
setVideoEncoder(profile.videoCodec) | |
setAudioChannels(2) | |
setAudioSamplingRate(44100) | |
setAudioEncoder(MediaRecorder.AudioEncoder.AAC) | |
setAudioEncodingBitRate(128000) | |
}.also { | |
it.prepare() | |
it.start() | |
} | |
fragment.binding.sceneView.startMirroringToSurface( | |
mediaRecorder!!.surface, | |
0, 0, | |
width, | |
height | |
) | |
} | |
}) | |
} | |
fun stopVideoRecord(fragment: Fragment) { | |
fragment.lifecycleScope.launchWhenCreated { | |
fragment.binding.sceneView.stopMirroringToSurface(mediaRecorder!!.surface) | |
mediaRecorder?.stop() | |
mediaRecorder?.reset() | |
recordFileDescriptor?.close() | |
fragment.context.contentResolver.update(recordUri!!, ContentValues().apply { | |
put(MediaStore.Video.Media.IS_PENDING, 0) | |
}, null, null) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment