Simplify ChunkedRecorder
This commit is contained in:
parent
d95057fa47
commit
08f37070a4
@ -1,68 +1,76 @@
|
|||||||
package com.mrousavy.camera.core
|
package com.mrousavy.camera.core
|
||||||
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import android.media.MediaCodec
|
import android.media.MediaCodec
|
||||||
|
import android.media.MediaCodec.BufferInfo
|
||||||
import android.media.MediaCodecInfo
|
import android.media.MediaCodecInfo
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
import android.media.MediaMuxer
|
import android.media.MediaMuxer
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.util.Size
|
import android.util.Size
|
||||||
import android.view.Surface
|
import android.view.Surface
|
||||||
import java.io.File
|
|
||||||
import com.mrousavy.camera.types.Orientation
|
import com.mrousavy.camera.types.Orientation
|
||||||
import com.mrousavy.camera.types.RecordVideoOptions
|
import com.mrousavy.camera.types.RecordVideoOptions
|
||||||
|
import java.io.File
|
||||||
import java.nio.ByteBuffer
|
import java.nio.ByteBuffer
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
|
||||||
class ChunkedRecordingManager(
|
class ChunkedRecordingManager(private val encoder: MediaCodec, private val outputDirectory: File, private val orientationHint: Int, private val iFrameInterval: Int) :
|
||||||
private val encoder: MediaCodec,
|
MediaCodec.Callback() {
|
||||||
private val outputDirectory: File,
|
|
||||||
private val orientationHint: Int,
|
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "ChunkedRecorder"
|
private const val TAG = "ChunkedRecorder"
|
||||||
private const val targetDurationUs = 10 * 1000000
|
|
||||||
|
|
||||||
fun fromParams(
|
fun fromParams(
|
||||||
size: Size,
|
size: Size,
|
||||||
enableAudio: Boolean,
|
enableAudio: Boolean,
|
||||||
fps: Int? = null,
|
fps: Int? = null,
|
||||||
orientation: Orientation,
|
orientation: Orientation,
|
||||||
|
bitRate: Int,
|
||||||
options: RecordVideoOptions,
|
options: RecordVideoOptions,
|
||||||
outputDirectory: File,
|
outputDirectory: File,
|
||||||
|
iFrameInterval: Int = 3
|
||||||
): ChunkedRecordingManager {
|
): ChunkedRecordingManager {
|
||||||
|
|
||||||
val mimeType = options.videoCodec.toMimeType()
|
val mimeType = options.videoCodec.toMimeType()
|
||||||
val format = MediaFormat.createVideoFormat(mimeType, size.width, size.height)
|
var width = size.width
|
||||||
|
var height = size.height
|
||||||
|
|
||||||
|
val orientationDegrees = orientation.toDegrees()
|
||||||
|
|
||||||
|
if (orientationDegrees == 90 || orientationDegrees == 270) {
|
||||||
|
width = size.height
|
||||||
|
height = size.width
|
||||||
|
}
|
||||||
|
val format = MediaFormat.createVideoFormat(mimeType, width, height)
|
||||||
|
|
||||||
val codec = MediaCodec.createEncoderByType(mimeType)
|
val codec = MediaCodec.createEncoderByType(mimeType)
|
||||||
|
|
||||||
// Set some properties. Failing to specify some of these can cause the MediaCodec
|
// Set some properties. Failing to specify some of these can cause the MediaCodec
|
||||||
// configure() call to throw an unhelpful exception.
|
// configure() call to throw an unhelpful exception.
|
||||||
format.setInteger(
|
format.setInteger(
|
||||||
MediaFormat.KEY_COLOR_FORMAT,
|
MediaFormat.KEY_COLOR_FORMAT,
|
||||||
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||||
)
|
)
|
||||||
fps?.apply {
|
fps?.apply {
|
||||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, this)
|
format.setInteger(MediaFormat.KEY_FRAME_RATE, this)
|
||||||
}
|
}
|
||||||
// TODO: Pull this out into configuration
|
// TODO: Pull this out into configuration
|
||||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
|
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, iFrameInterval)
|
||||||
|
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate)
|
||||||
|
|
||||||
|
Log.i(TAG, "Video Format: $format")
|
||||||
Log.d(TAG, "Video Format: $format")
|
|
||||||
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
|
// Create a MediaCodec encoder, and configure it with our format. Get a Surface
|
||||||
// we can use for input and wrap it with a class that handles the EGL work.
|
// we can use for input and wrap it with a class that handles the EGL work.
|
||||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||||
return ChunkedRecordingManager(codec, outputDirectory, orientation.toDegrees())
|
return ChunkedRecordingManager(codec, outputDirectory, orientationDegrees, iFrameInterval)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// In flight details
|
// In flight details
|
||||||
private val bufferInfo = MediaCodec.BufferInfo()
|
|
||||||
private var currentFrameNumber: Int = 0
|
private var currentFrameNumber: Int = 0
|
||||||
private var chunkIndex = 0
|
private var chunkIndex = -1
|
||||||
private var encodedFormat: MediaFormat? = null
|
private var encodedFormat: MediaFormat? = null
|
||||||
|
private var recording = false;
|
||||||
|
|
||||||
|
private val targetDurationUs = iFrameInterval * 1000000
|
||||||
|
|
||||||
val surface: Surface = encoder.createInputSurface()
|
val surface: Surface = encoder.createInputSurface()
|
||||||
|
|
||||||
@ -70,15 +78,11 @@ class ChunkedRecordingManager(
|
|||||||
if (!this.outputDirectory.exists()) {
|
if (!this.outputDirectory.exists()) {
|
||||||
this.outputDirectory.mkdirs()
|
this.outputDirectory.mkdirs()
|
||||||
}
|
}
|
||||||
|
encoder.setCallback(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Muxer specific
|
// Muxer specific
|
||||||
private class MuxerContext(
|
private class MuxerContext(val muxer: MediaMuxer, startTimeUs: Long, encodedFormat: MediaFormat) {
|
||||||
muxer: MediaMuxer,
|
|
||||||
startTimeUs: Long,
|
|
||||||
encodedFormat: MediaFormat
|
|
||||||
) {
|
|
||||||
val muxer = muxer
|
|
||||||
val videoTrack: Int = muxer.addTrack(encodedFormat)
|
val videoTrack: Int = muxer.addTrack(encodedFormat)
|
||||||
val startTimeUs: Long = startTimeUs
|
val startTimeUs: Long = startTimeUs
|
||||||
|
|
||||||
@ -86,135 +90,80 @@ class ChunkedRecordingManager(
|
|||||||
muxer.start()
|
muxer.start()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun finish() {
|
fun finish() {
|
||||||
muxer.stop()
|
muxer.stop()
|
||||||
muxer.release()
|
muxer.release()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private lateinit var muxerContext: MuxerContext
|
private var muxerContext: MuxerContext? = null
|
||||||
|
|
||||||
private fun createNextMuxer() {
|
private fun createNextMuxer(bufferInfo: BufferInfo) {
|
||||||
if (::muxerContext.isInitialized) {
|
muxerContext?.finish()
|
||||||
muxerContext.finish()
|
chunkIndex++
|
||||||
chunkIndex++
|
|
||||||
}
|
|
||||||
|
|
||||||
val newFileName = "$chunkIndex.mp4"
|
val newFileName = "$chunkIndex.mp4"
|
||||||
val newOutputFile = File(this.outputDirectory, newFileName)
|
val newOutputFile = File(this.outputDirectory, newFileName)
|
||||||
Log.d(TAG, "Creating new muxer for file: $newFileName")
|
Log.i(TAG, "Creating new muxer for file: $newFileName")
|
||||||
val muxer = MediaMuxer(
|
val muxer = MediaMuxer(
|
||||||
newOutputFile.absolutePath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
|
newOutputFile.absolutePath,
|
||||||
|
MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4
|
||||||
)
|
)
|
||||||
|
muxer.setOrientationHint(orientationHint)
|
||||||
muxerContext = MuxerContext(
|
muxerContext = MuxerContext(
|
||||||
muxer, bufferInfo.presentationTimeUs, this.encodedFormat!!
|
muxer, bufferInfo.presentationTimeUs, this.encodedFormat!!
|
||||||
)
|
)
|
||||||
muxer.setOrientationHint(orientationHint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun atKeyframe(): Boolean {
|
private fun atKeyframe(bufferInfo: BufferInfo): Boolean {
|
||||||
return (bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0
|
return (bufferInfo.flags and MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun chunkLengthUs(): Long {
|
private fun chunkLengthUs(bufferInfo: BufferInfo): Long {
|
||||||
return bufferInfo.presentationTimeUs - muxerContext.startTimeUs
|
return bufferInfo.presentationTimeUs - muxerContext!!.startTimeUs
|
||||||
}
|
}
|
||||||
|
|
||||||
fun drainEncoder(): Boolean {
|
fun start() {
|
||||||
val timeout: Long = 0
|
encoder.start()
|
||||||
var frameWasEncoded = false
|
recording = true
|
||||||
|
|
||||||
while (true) {
|
|
||||||
var encoderStatus: Int = encoder.dequeueOutputBuffer(bufferInfo, timeout)
|
|
||||||
|
|
||||||
if (encoderStatus < 0) {
|
|
||||||
Log.w(
|
|
||||||
TAG, "Unexpected result from encoder.dequeueOutputBuffer: $encoderStatus"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
when (encoderStatus) {
|
|
||||||
MediaCodec.INFO_TRY_AGAIN_LATER -> break;
|
|
||||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
|
||||||
// Should happen before receiving buffers, and should only happen once. The MediaFormat
|
|
||||||
// contains the csd-0 and csd-1 keys, which we'll need for MediaMuxer. It's unclear what
|
|
||||||
// else MediaMuxer might want, so rather than extract the codec-specific data and
|
|
||||||
// reconstruct a new MediaFormat later, we just grab it here and keep it around.
|
|
||||||
encodedFormat = encoder.outputFormat
|
|
||||||
Log.d(TAG, "encoder output format changed: $encodedFormat")
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
var encodedData: ByteBuffer = encoder.getOutputBuffer(encoderStatus)
|
|
||||||
?: throw RuntimeException("encoderOutputBuffer $encoderStatus was null")
|
|
||||||
|
|
||||||
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
|
||||||
// The codec config data was pulled out when we got the
|
|
||||||
// INFO_OUTPUT_FORMAT_CHANGED status. The MediaMuxer won't accept
|
|
||||||
// a single big blob -- it wants separate csd-0/csd-1 chunks --
|
|
||||||
// so simply saving this off won't work.
|
|
||||||
Log.d(TAG, "ignoring BUFFER_FLAG_CODEC_CONFIG")
|
|
||||||
bufferInfo.size = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bufferInfo.size != 0) {
|
|
||||||
// adjust the ByteBuffer values to match BufferInfo (not needed?)
|
|
||||||
encodedData.position(bufferInfo.offset)
|
|
||||||
encodedData.limit(bufferInfo.offset + bufferInfo.size)
|
|
||||||
|
|
||||||
if (!::muxerContext.isInitialized || (atKeyframe() && chunkLengthUs() >= targetDurationUs)) {
|
|
||||||
this.createNextMuxer()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: we should probably add the presentation time stamp
|
|
||||||
// mEncBuffer.add(encodedData, bufferInfo.flags, bufferInfo.presentationTimeUs)
|
|
||||||
|
|
||||||
muxerContext.muxer.writeSampleData(muxerContext.videoTrack, encodedData, bufferInfo)
|
|
||||||
frameWasEncoded = true
|
|
||||||
}
|
|
||||||
|
|
||||||
encoder.releaseOutputBuffer(encoderStatus, false)
|
|
||||||
|
|
||||||
if ((bufferInfo.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) {
|
|
||||||
Log.w(TAG, "reached end of stream unexpectedly")
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return frameWasEncoded
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun finish() {
|
fun finish() {
|
||||||
if (::muxerContext.isInitialized) {
|
synchronized(this) {
|
||||||
muxerContext.finish()
|
muxerContext?.finish()
|
||||||
|
recording = false
|
||||||
|
muxerContext = null
|
||||||
|
encoder.stop()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// MediaCodec.Callback methods
|
||||||
class ChunkedRecorder(private val manager: ChunkedRecordingManager) {
|
override fun onInputBufferAvailable(codec: MediaCodec, index: Int) {
|
||||||
private val messageChannel = Channel<Message>()
|
}
|
||||||
|
|
||||||
init {
|
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, bufferInfo: MediaCodec.BufferInfo) {
|
||||||
CoroutineScope(Dispatchers.Default).launch {
|
synchronized(this) {
|
||||||
for (msg in messageChannel) {
|
if (!recording) {
|
||||||
when (msg) {
|
return
|
||||||
is Message.FrameAvailable -> manager.drainEncoder()
|
}
|
||||||
is Message.Shutdown -> manager.finish()
|
val encodedData: ByteBuffer = encoder.getOutputBuffer(index)
|
||||||
}
|
?: throw RuntimeException("getOutputBuffer was null")
|
||||||
}
|
|
||||||
}
|
if (muxerContext == null || (atKeyframe(bufferInfo) && chunkLengthUs(bufferInfo) >= targetDurationUs)) {
|
||||||
}
|
this.createNextMuxer(bufferInfo)
|
||||||
|
}
|
||||||
fun sendFrameAvailable() {
|
muxerContext!!.muxer.writeSampleData(muxerContext!!.videoTrack, encodedData, bufferInfo)
|
||||||
messageChannel.trySend(Message.FrameAvailable)
|
encoder.releaseOutputBuffer(index, false)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
fun sendShutdown() {
|
|
||||||
messageChannel.trySend(Message.Shutdown)
|
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
|
||||||
}
|
// Implement error handling
|
||||||
|
Log.e(TAG, "Codec error: ${e.message}")
|
||||||
sealed class Message {
|
}
|
||||||
object FrameAvailable : Message()
|
|
||||||
object Shutdown : Message()
|
override fun onOutputFormatChanged(codec: MediaCodec, format: MediaFormat) {
|
||||||
}
|
encodedFormat = format
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,10 @@ import com.mrousavy.camera.types.Orientation
|
|||||||
import com.mrousavy.camera.types.RecordVideoOptions
|
import com.mrousavy.camera.types.RecordVideoOptions
|
||||||
import com.mrousavy.camera.utils.FileUtils
|
import com.mrousavy.camera.utils.FileUtils
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import android.os.Environment
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Locale
|
||||||
|
import java.util.Date
|
||||||
class RecordingSession(
|
class RecordingSession(
|
||||||
context: Context,
|
context: Context,
|
||||||
val cameraId: String,
|
val cameraId: String,
|
||||||
@ -33,23 +36,34 @@ class RecordingSession(
|
|||||||
|
|
||||||
data class Video(val path: String, val durationMs: Long, val size: Size)
|
data class Video(val path: String, val durationMs: Long, val size: Size)
|
||||||
|
|
||||||
private val outputPath = File.createTempFile("mrousavy", options.fileType.toExtension(), context.cacheDir)
|
private val outputPath = run {
|
||||||
|
val videoDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_MOVIES)
|
||||||
|
val sdf = SimpleDateFormat("yyyy_MM_dd_HH_mm_ss_SSS", Locale.US)
|
||||||
|
val videoFileName = "VID_${sdf.format(Date())}"
|
||||||
|
File(videoDir!!, videoFileName)
|
||||||
|
}
|
||||||
|
|
||||||
private val bitRate = getBitRate()
|
private val bitRate = getBitRate()
|
||||||
private val recordingManager = ChunkedRecordingManager.fromParams(
|
private val recorder = ChunkedRecordingManager.fromParams(
|
||||||
size, enableAudio, fps, orientation, options, outputPath
|
size,
|
||||||
|
enableAudio,
|
||||||
|
fps,
|
||||||
|
orientation,
|
||||||
|
bitRate,
|
||||||
|
options,
|
||||||
|
outputPath
|
||||||
)
|
)
|
||||||
private val recorder: ChunkedRecorder = ChunkedRecorder(recordingManager)
|
|
||||||
private var startTime: Long? = null
|
private var startTime: Long? = null
|
||||||
val surface: Surface
|
val surface: Surface
|
||||||
get() {
|
get() {
|
||||||
return recordingManager.surface
|
return recorder.surface
|
||||||
}
|
}
|
||||||
|
|
||||||
fun start() {
|
fun start() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
Log.i(TAG, "Starting RecordingSession..")
|
Log.i(TAG, "Starting RecordingSession..")
|
||||||
startTime = System.currentTimeMillis()
|
startTime = System.currentTimeMillis()
|
||||||
|
recorder.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -57,14 +71,15 @@ class RecordingSession(
|
|||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
Log.i(TAG, "Stopping RecordingSession..")
|
Log.i(TAG, "Stopping RecordingSession..")
|
||||||
try {
|
try {
|
||||||
recorder.sendShutdown()
|
recorder.finish()
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
Log.e(TAG, "Failed to stop MediaRecorder!", e)
|
Log.e(TAG, "Failed to stop MediaRecorder!", e)
|
||||||
}
|
}
|
||||||
|
|
||||||
val stopTime = System.currentTimeMillis()
|
val stopTime = System.currentTimeMillis()
|
||||||
val durationMs = stopTime - (startTime ?: stopTime)
|
val durationMs = stopTime - (startTime ?: stopTime)
|
||||||
//callback(Video(outputFile.absolutePath, durationMs, size))
|
Log.i(TAG, "Finished recording video at $outputPath")
|
||||||
|
callback(Video(outputPath.absolutePath, durationMs, size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -113,6 +128,5 @@ class RecordingSession(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun onFrame() {
|
fun onFrame() {
|
||||||
recorder.sendFrameAvailable()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -91,7 +91,7 @@ class VideoPipeline(
|
|||||||
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES)
|
imageWriter = ImageWriter.newInstance(glSurface, MAX_IMAGES)
|
||||||
}
|
}
|
||||||
imageReader!!.setOnImageAvailableListener({ reader ->
|
imageReader!!.setOnImageAvailableListener({ reader ->
|
||||||
Log.i(TAG, "ImageReader::onImageAvailable!")
|
// Log.i(TAG, "ImageReader::onImageAvailable!")s
|
||||||
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
val image = reader.acquireNextImage() ?: return@setOnImageAvailableListener
|
||||||
|
|
||||||
// TODO: Get correct orientation and isMirrored
|
// TODO: Get correct orientation and isMirrored
|
||||||
@ -153,7 +153,8 @@ class VideoPipeline(
|
|||||||
// 5. Draw it with applied rotation/mirroring
|
// 5. Draw it with applied rotation/mirroring
|
||||||
onFrame(transformMatrix)
|
onFrame(transformMatrix)
|
||||||
|
|
||||||
recording?.onFrame()
|
// 6. Notify the recording session.
|
||||||
|
recordingSession?.onFrame()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
package com.mrousavy.camera.types
|
package com.mrousavy.camera.types
|
||||||
|
|
||||||
import android.media.MediaRecorder
|
|
||||||
import android.media.MediaFormat
|
import android.media.MediaFormat
|
||||||
|
import android.media.MediaRecorder
|
||||||
|
|
||||||
enum class VideoCodec(override val unionValue: String) : JSUnionValue {
|
enum class VideoCodec(override val unionValue: String) : JSUnionValue {
|
||||||
H264("h264"),
|
H264("h264"),
|
||||||
|
@ -160,9 +160,9 @@ export function CameraPage({ navigation }: Props): React.ReactElement {
|
|||||||
const frameProcessor = useFrameProcessor((frame) => {
|
const frameProcessor = useFrameProcessor((frame) => {
|
||||||
'worklet'
|
'worklet'
|
||||||
|
|
||||||
console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`)
|
// console.log(`${frame.timestamp}: ${frame.width}x${frame.height} ${frame.pixelFormat} Frame (${frame.orientation})`)
|
||||||
examplePlugin(frame)
|
// examplePlugin(frame)
|
||||||
exampleKotlinSwiftPlugin(frame)
|
// exampleKotlinSwiftPlugin(frame)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -74,7 +74,7 @@ export function MediaPage({ navigation, route }: Props): React.ReactElement {
|
|||||||
}
|
}
|
||||||
}, [path, type])
|
}, [path, type])
|
||||||
|
|
||||||
const source = useMemo(() => ({ uri: `file://${path}` }), [path])
|
const source = useMemo(() => ({ uri: `file://${path}/1.mp4` }), [path])
|
||||||
|
|
||||||
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded])
|
const screenStyle = useMemo(() => ({ opacity: hasMediaLoaded ? 1 : 0 }), [hasMediaLoaded])
|
||||||
|
|
||||||
|
@ -11,59 +11,67 @@
|
|||||||
url = "github:tadfisher/android-nixpkgs";
|
url = "github:tadfisher/android-nixpkgs";
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, flake-utils, gitignore, android-nixpkgs, ... }:
|
outputs = {
|
||||||
flake-utils.lib.eachDefaultSystem (system:
|
self,
|
||||||
let
|
nixpkgs,
|
||||||
pkgs = import nixpkgs { inherit system; };
|
flake-utils,
|
||||||
nodejs = pkgs.nodejs-18_x;
|
gitignore,
|
||||||
# NOTE: this does not work
|
android-nixpkgs,
|
||||||
appBuild = pkgs.stdenv.mkDerivation {
|
...
|
||||||
name = "example-ts-node";
|
}:
|
||||||
version = "0.1.0";
|
flake-utils.lib.eachDefaultSystem (system: let
|
||||||
src = gitignore.lib.gitignoreSource ./.; # uses the gitignore in the repo to only copy files git would see
|
pkgs = import nixpkgs {inherit system;};
|
||||||
buildInputs = [ nodejs ];
|
nodejs = pkgs.nodejs-18_x;
|
||||||
# https://nixos.org/manual/nixpkgs/stable/#sec-stdenv-phases
|
# NOTE: this does not work
|
||||||
buildPhase = ''
|
appBuild = pkgs.stdenv.mkDerivation {
|
||||||
# each phase has pre/postHooks. When you make your own phase be sure to still call the hooks
|
name = "example-ts-node";
|
||||||
runHook preBuild
|
version = "0.1.0";
|
||||||
npm ci
|
src = gitignore.lib.gitignoreSource ./.; # uses the gitignore in the repo to only copy files git would see
|
||||||
npm run build
|
buildInputs = [nodejs];
|
||||||
runHook postBuild
|
# https://nixos.org/manual/nixpkgs/stable/#sec-stdenv-phases
|
||||||
'';
|
buildPhase = ''
|
||||||
installPhase = ''
|
# each phase has pre/postHooks. When you make your own phase be sure to still call the hooks
|
||||||
runHook preInstall
|
runHook preBuild
|
||||||
cp -r node_modules $out/node_modules
|
npm ci
|
||||||
cp package.json $out/package.json
|
npm run build
|
||||||
cp -r dist $out/dist
|
runHook postBuild
|
||||||
runHook postInstall
|
'';
|
||||||
'';
|
installPhase = ''
|
||||||
};
|
runHook preInstall
|
||||||
android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs: with sdkPkgs; [
|
cp -r node_modules $out/node_modules
|
||||||
cmdline-tools-latest
|
cp package.json $out/package.json
|
||||||
build-tools-30-0-3
|
cp -r dist $out/dist
|
||||||
build-tools-33-0-0
|
runHook postInstall
|
||||||
build-tools-33-0-1
|
'';
|
||||||
build-tools-34-0-0
|
};
|
||||||
platform-tools
|
android-sdk = android-nixpkgs.sdk.${system} (sdkPkgs:
|
||||||
platforms-android-33
|
with sdkPkgs; [
|
||||||
platforms-android-34
|
cmdline-tools-latest
|
||||||
emulator
|
build-tools-30-0-3
|
||||||
ndk-23-1-7779620
|
build-tools-33-0-0
|
||||||
cmake-3-22-1
|
build-tools-33-0-1
|
||||||
system-images-android-33-google-apis-x86-64
|
build-tools-34-0-0
|
||||||
system-images-android-34-google-apis-x86-64
|
platform-tools
|
||||||
]);
|
platforms-android-33
|
||||||
in with pkgs; {
|
platforms-android-34
|
||||||
defaultPackage = appBuild;
|
emulator
|
||||||
devShell = mkShell {
|
ndk-23-1-7779620
|
||||||
buildInputs = [ nodejs yarn watchman gradle_7 alejandra nodePackages.prettier ];
|
cmake-3-22-1
|
||||||
ANDROID_SDK_BIN = android-sdk;
|
system-images-android-33-google-apis-x86-64
|
||||||
shellHook = ''
|
system-images-android-34-google-apis-x86-64
|
||||||
export JAVA_HOME=${pkgs.jdk17.home}
|
]);
|
||||||
source ${android-sdk.out}/nix-support/setup-hook
|
in
|
||||||
export PATH=${android-sdk}/bin:$PATH
|
with pkgs; {
|
||||||
ORG_GRADLE_PROJECT_ANDROID_HOME="$ANDROID_HOME"
|
defaultPackage = appBuild;
|
||||||
'';
|
devShell = mkShell {
|
||||||
};
|
buildInputs = [nodejs yarn watchman gradle_7 alejandra nodePackages.prettier ktlint kotlin-language-server];
|
||||||
});
|
ANDROID_SDK_BIN = android-sdk;
|
||||||
|
shellHook = ''
|
||||||
|
export JAVA_HOME=${pkgs.jdk17.home}
|
||||||
|
source ${android-sdk.out}/nix-support/setup-hook
|
||||||
|
export PATH=${android-sdk}/bin:$PATH
|
||||||
|
ORG_GRADLE_PROJECT_ANDROID_HOME="$ANDROID_HOME"
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user