Comments added, code cleared
This commit is contained in:
@@ -4,31 +4,36 @@ import android.content.Context
|
||||
import android.graphics.*
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import android.view.SurfaceHolder
|
||||
import android.view.SurfaceView
|
||||
import kotlinx.coroutines.*
|
||||
import kotlin.system.measureTimeMillis
|
||||
|
||||
|
||||
/**
|
||||
* Класс наследован от SurfaceView
|
||||
* В нем отрисовывается проход фразы Hello World за 20 кадров
|
||||
* в превью можно переключать кадры, меняя _frameNumber от 0 до 19
|
||||
* В режиме редактирования для удобной отладки строка заполняется красным фоном
|
||||
*
|
||||
*/
|
||||
class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
|
||||
private var _helloWorldString: String = ""
|
||||
private var _textSize: Float = 100f
|
||||
private var _frameNumber: Int = 7
|
||||
private var _helloWorldString: String = "" //Строка для отображения
|
||||
private var _frameNumber: Int = 7 // Инициализация номера фрейма, видно на превью
|
||||
private var _frameRate: Int = 30 //Частота кадров
|
||||
|
||||
private lateinit var textPaint: TextPaint
|
||||
private lateinit var backgroundPaint: Paint
|
||||
private lateinit var backgroundPaint: Paint //Фон надписи в режиме редактирвоания
|
||||
private var textWidth: Float = 0f
|
||||
private var textHeight: Float = 0f
|
||||
private var textPositionVertical = 0f
|
||||
private var frameDelta = 0f
|
||||
|
||||
private var mainJob: Job? = null
|
||||
private var persistentSurface: Surface? = null
|
||||
|
||||
private var persistentSurface: Surface? = null //Surface для рендера и сохранения видео
|
||||
|
||||
/**
|
||||
* The text to draw
|
||||
* Текст для отрисовки
|
||||
*/
|
||||
var helloWorldString: String
|
||||
get() = _helloWorldString
|
||||
@@ -38,14 +43,17 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
/**
|
||||
* font Size
|
||||
* Частота кадров
|
||||
*/
|
||||
var textSize: Float
|
||||
get() = _textSize
|
||||
var frameRate: Int
|
||||
get() = _frameRate
|
||||
set(value) {
|
||||
_textSize = value
|
||||
invalidateTextPaintAndMeasurements()
|
||||
_frameRate = value
|
||||
}
|
||||
|
||||
/**
|
||||
* Номер фрейма, используется только в режиме редактирования для превью
|
||||
*/
|
||||
var frameNumber: Int
|
||||
get() = _frameNumber
|
||||
set(value) {
|
||||
@@ -53,7 +61,6 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
|
||||
|
||||
init {
|
||||
holder.addCallback(this)
|
||||
}
|
||||
@@ -75,20 +82,14 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
}
|
||||
|
||||
private fun attrsInit(attrs: AttributeSet?, defStyle: Int) {
|
||||
// Load attributes
|
||||
|
||||
val a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.TestView, defStyle, 0
|
||||
)
|
||||
|
||||
_helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String"
|
||||
_textSize = a.getDimension(
|
||||
R.styleable.TestView_textSize,
|
||||
textSize
|
||||
)
|
||||
_frameNumber = a.getInteger(
|
||||
R.styleable.TestView_frameNumber,
|
||||
frameNumber
|
||||
)
|
||||
_frameRate = a.getInteger(R.styleable.TestView_frameRate, 30)
|
||||
_frameNumber = a.getInteger(R.styleable.TestView_frameNumber, frameNumber)
|
||||
a.recycle()
|
||||
|
||||
textPaint = TextPaint().apply {
|
||||
@@ -103,17 +104,20 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
|
||||
fun setExternalSurface(surface: Surface) {
|
||||
persistentSurface = surface
|
||||
}
|
||||
|
||||
/**
|
||||
* Перегруженный onDraw остался только для превью
|
||||
*/
|
||||
override fun onDraw(canvas: Canvas?) {
|
||||
super.onDraw(canvas)
|
||||
if (isInEditMode)
|
||||
canvas?.let {
|
||||
drawHello(canvas, _frameNumber)
|
||||
}
|
||||
|
||||
}
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
@@ -121,38 +125,46 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
}
|
||||
private fun invalidateTextPaintAndMeasurements() {
|
||||
val textBounds = Rect()
|
||||
textPaint.let {
|
||||
it.textSize = textSize
|
||||
}
|
||||
textPaint.getTextBounds(helloWorldString, 0, helloWorldString.length, textBounds)
|
||||
|
||||
textPaint.getTextBounds(_helloWorldString, 0, _helloWorldString.length, textBounds)
|
||||
textHeight = textBounds.height()+0f
|
||||
textWidth = textBounds.width()+0f
|
||||
}
|
||||
|
||||
/**
|
||||
* Выделение красным фоном надписи в превью
|
||||
*/
|
||||
private fun drawBounds(canvas: Canvas, sx: Int, sy: Int) {
|
||||
val textBounds = Rect()
|
||||
textPaint.getTextBounds(helloWorldString, 0, helloWorldString.length, textBounds)
|
||||
textBounds.right = textPaint.measureText(helloWorldString).toInt()
|
||||
textPaint.getTextBounds(_helloWorldString, 0, _helloWorldString.length, textBounds)
|
||||
textBounds.right = textPaint.measureText(_helloWorldString).toInt()
|
||||
textBounds.offsetTo(sx, sy-textHeight.toInt()+textPaint.descent().toInt())
|
||||
canvas.drawRect(textBounds, backgroundPaint)
|
||||
}
|
||||
/**
|
||||
* todo comment
|
||||
*/
|
||||
|
||||
fun drawHello(canvas: Canvas, frameNumber: Int) {
|
||||
/**
|
||||
* Основной метод прорисовки надписи
|
||||
* Так как этот метод используется для различных размеров Canvas,
|
||||
* я решил сделать размер шрифта равным 1/20 от высоты Canvas
|
||||
*
|
||||
*/
|
||||
private fun drawHello(canvas: Canvas, frameN: Int) {
|
||||
|
||||
val textSize = canvas.height/20f
|
||||
textPaint.textSize = textSize
|
||||
textWidth = textPaint.measureText(helloWorldString)
|
||||
textWidth = textPaint.measureText(_helloWorldString)
|
||||
|
||||
textPositionVertical = canvas.height/2f+textSize/2f
|
||||
frameDelta = (canvas.width+textWidth)/20f
|
||||
val textPositionVertical = canvas.height/2f+textSize/2f
|
||||
val frameDelta = (canvas.width+textWidth)/20f
|
||||
|
||||
var xPos = frameNumber*frameDelta-textWidth
|
||||
val xPos = frameN*frameDelta-textWidth
|
||||
|
||||
if (isInEditMode) drawBounds(canvas, xPos.toInt(), textPositionVertical.toInt())
|
||||
if (isInEditMode) {
|
||||
invalidateTextPaintAndMeasurements()
|
||||
drawBounds(canvas, xPos.toInt(), textPositionVertical.toInt())
|
||||
}
|
||||
|
||||
canvas.drawText( helloWorldString, xPos, textPositionVertical, textPaint)
|
||||
canvas.drawText( _helloWorldString, xPos, textPositionVertical, textPaint)
|
||||
|
||||
}
|
||||
private fun clearCanvas(canvas: Canvas) {
|
||||
@@ -165,20 +177,31 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
renderLoop()
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile private var maxFramesRender = 120
|
||||
@Volatile private var framesRendered = -1
|
||||
|
||||
|
||||
/**
|
||||
* Метод, запускающий отсчет фреймов для записи
|
||||
*/
|
||||
fun startRecording(maxFrames: Int) {
|
||||
maxFramesRender = maxFrames
|
||||
framesRendered++
|
||||
}
|
||||
var onLastFrame : () -> Unit = @Synchronized {}
|
||||
|
||||
/**
|
||||
* Метод, который будет вызван из корутины после рендера последнего фрейма для записи
|
||||
* Он будет переназначен во время инициализации енкодера
|
||||
*/
|
||||
var onLastFrame : () -> Unit = {}
|
||||
|
||||
fun setOnLastFrameRecordedListener(listener: () -> Unit) {
|
||||
onLastFrame = listener
|
||||
}
|
||||
|
||||
/**
|
||||
* Отрисовка превью и здесь же рендер фреймов
|
||||
*/
|
||||
private suspend fun renderLoop() {
|
||||
while(GlobalScope.isActive) {
|
||||
val timeInMillis = measureTimeMillis {
|
||||
@@ -193,13 +216,15 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
preview()
|
||||
|
||||
_frameNumber++
|
||||
}
|
||||
if (_frameNumber >= 20) _frameNumber = 0
|
||||
delay(33-timeInMillis)
|
||||
}
|
||||
//Расчет паузы перед отрисовкой следующего фрейма
|
||||
delay(1000/_frameRate-timeInMillis)
|
||||
|
||||
}
|
||||
}
|
||||
fun render() {
|
||||
//Рендер фрейма для записи видео
|
||||
private fun render() {
|
||||
persistentSurface?.let {surface ->
|
||||
val pCanvas = surface.lockCanvas(null)
|
||||
pCanvas?.let {
|
||||
@@ -211,8 +236,8 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun preview() {
|
||||
//Такой же рендер, но видимый на View и работающий постоянно
|
||||
private fun preview() {
|
||||
val canvas = holder.lockCanvas()
|
||||
canvas?.let {
|
||||
clearCanvas(it)
|
||||
@@ -224,7 +249,7 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
|
||||
}
|
||||
|
||||
//Остановка корутины при уничтожении surface
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
runBlocking {
|
||||
mainJob?.cancelAndJoin()
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
package su.rst10h.loopedworld
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Typeface
|
||||
import android.text.TextPaint
|
||||
import android.util.AttributeSet
|
||||
import android.util.Log
|
||||
import android.view.SurfaceView
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* TODO: document your custom view class.
|
||||
*/
|
||||
class TestView : View {
|
||||
|
||||
private var _helloWorldString: String = ""
|
||||
private var _textSize: Float = 100f // TODO: use a default from R.dimen...
|
||||
private var _frameNumber: Int = 12
|
||||
|
||||
private lateinit var textPaint: TextPaint
|
||||
private var textWidth: Float = 0f
|
||||
private var textHeight: Float = 0f
|
||||
private var textPositionVertical = 0f
|
||||
private var frameDelta = 0f
|
||||
|
||||
/**
|
||||
* The text to draw
|
||||
*/
|
||||
var helloWorldString: String
|
||||
get() = _helloWorldString
|
||||
set(value) {
|
||||
_helloWorldString = value
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
|
||||
/**
|
||||
* font Size
|
||||
*/
|
||||
var textSize: Float
|
||||
get() = _textSize
|
||||
set(value) {
|
||||
_textSize = value
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
var frameNumber: Int
|
||||
get() = _frameNumber
|
||||
set(value) {
|
||||
_frameNumber = value
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
constructor(context: Context) : super(context) {
|
||||
init(null, 0)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
|
||||
init(attrs, 0)
|
||||
}
|
||||
|
||||
constructor(context: Context, attrs: AttributeSet, defStyle: Int) : super(
|
||||
context,
|
||||
attrs,
|
||||
defStyle
|
||||
) {
|
||||
init(attrs, defStyle)
|
||||
}
|
||||
|
||||
private fun init(attrs: AttributeSet?, defStyle: Int) {
|
||||
// Load attributes
|
||||
val a = context.obtainStyledAttributes(
|
||||
attrs, R.styleable.TestView, defStyle, 0
|
||||
)
|
||||
|
||||
_helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String Hello"
|
||||
_textSize = a.getDimension(
|
||||
R.styleable.TestView_textSize,
|
||||
textSize
|
||||
)
|
||||
_frameNumber = a.getInteger(
|
||||
R.styleable.TestView_frameNumber,
|
||||
frameNumber
|
||||
)
|
||||
a.recycle()
|
||||
|
||||
textPaint = TextPaint().apply {
|
||||
flags = Paint.ANTI_ALIAS_FLAG
|
||||
textAlign = Paint.Align.LEFT
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
}
|
||||
|
||||
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
invalidateTextPaintAndMeasurements()
|
||||
}
|
||||
private fun invalidateTextPaintAndMeasurements() {
|
||||
textPaint.let {
|
||||
it.textSize = textSize
|
||||
textWidth = it.measureText(helloWorldString)
|
||||
textHeight = it.fontMetrics.bottom
|
||||
}
|
||||
invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* todo commen
|
||||
*/
|
||||
private fun drawHelloWorld(canvas: Canvas, frameNumber: Int) {
|
||||
|
||||
textPositionVertical = canvas.height/2f+textHeight
|
||||
frameDelta = (width+textWidth)/20f
|
||||
var xPos = frameNumber*frameDelta-textWidth
|
||||
// if (isInEditMode) {
|
||||
// xPos = width/2f-textWidth/2f
|
||||
// }
|
||||
helloWorldString.let {
|
||||
canvas.drawText( it, xPos, textPositionVertical, textPaint)
|
||||
}
|
||||
}
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
||||
super.onDraw(canvas)
|
||||
|
||||
// TODO: consider storing these as member variables to reduce
|
||||
|
||||
val paddingLeft = paddingLeft
|
||||
val paddingTop = paddingTop
|
||||
val paddingRight = paddingRight
|
||||
val paddingBottom = paddingBottom
|
||||
|
||||
val contentWidth = width - paddingLeft - paddingRight
|
||||
val contentHeight = height - paddingTop - paddingBottom
|
||||
|
||||
drawHelloWorld(canvas, _frameNumber)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<su.rst10h.loopedworld.TestView
|
||||
<su.rst10h.loopedworld.HelloSurface
|
||||
style="@style/Widget.Theme.Inspiry.MyView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
@@ -11,6 +11,7 @@
|
||||
android:paddingBottom="40dp"
|
||||
app:frameNumber="8"
|
||||
app:helloString="Hello World"
|
||||
app:textSize="55sp" />
|
||||
app:frameRate = "30"
|
||||
/>
|
||||
|
||||
</FrameLayout>
|
||||
@@ -1,7 +1,7 @@
|
||||
<resources>
|
||||
<declare-styleable name="TestView">
|
||||
<attr name="helloString" format="string" />
|
||||
<attr name="textSize" format="dimension" />
|
||||
<attr name="frameRate" format="integer" />
|
||||
<attr name="frameNumber" format="integer" />
|
||||
</declare-styleable>
|
||||
</resources>
|
||||
@@ -1,13 +1,11 @@
|
||||
package su.rst10h.inspiry
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
import org.junit.Assert.*
|
||||
|
||||
/**
|
||||
* Instrumented test, which will execute on an Android device.
|
||||
*
|
||||
|
||||
@@ -12,12 +12,23 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Inspiry">
|
||||
<activity android:name=".ui.MainActivity">
|
||||
<activity android:name=".ui.MainActivity"
|
||||
android:configChanges="orientation"
|
||||
android:screenOrientation="portrait">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<provider
|
||||
android:name="androidx.core.content.FileProvider"
|
||||
android:authorities="su.rst10h.inspiry.fileprovider"
|
||||
android:grantUriPermissions="true"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||
android:resource="@xml/filepaths" />
|
||||
</provider>
|
||||
</application>
|
||||
|
||||
</manifest>
|
||||
@@ -1,4 +1,11 @@
|
||||
package su.rst10h.inspiry
|
||||
|
||||
import android.media.MediaFormat
|
||||
|
||||
/**
|
||||
* Константы, доступные в любой части приложения.
|
||||
*/
|
||||
const val MEDIA_FOLDER = "test"
|
||||
const val MEDIA_FILE_NAME = "TestVideo.mp4"
|
||||
const val MEDIA_CODEC = MediaFormat.MIMETYPE_VIDEO_AVC
|
||||
const val MEDIA_FRAME_RATE = 30
|
||||
@@ -5,6 +5,11 @@ import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import androidx.core.app.ActivityCompat
|
||||
|
||||
|
||||
/**
|
||||
* Проверка разрешений.
|
||||
* Оставил на всякий случай
|
||||
*/
|
||||
class PermissionsChecker {
|
||||
companion object {
|
||||
private var REQUEST_EXTERNAL_STORAGE = 1
|
||||
|
||||
@@ -4,12 +4,13 @@ import android.media.MediaCodec
|
||||
import android.media.MediaCodecInfo
|
||||
import android.media.MediaFormat
|
||||
import android.media.MediaMuxer
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import su.rst10h.inspiry.MEDIA_CODEC
|
||||
import su.rst10h.inspiry.MEDIA_FILE_NAME
|
||||
import su.rst10h.inspiry.MEDIA_FOLDER
|
||||
import su.rst10h.inspiry.MEDIA_FRAME_RATE
|
||||
import su.rst10h.loopedworld.HelloSurface
|
||||
import java.io.File
|
||||
|
||||
@@ -17,29 +18,43 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
|
||||
|
||||
private val bufferInfo = MediaCodec.BufferInfo()
|
||||
private val mediaEncoder: MediaCodec
|
||||
private lateinit var inputSurface: Surface
|
||||
private val inputSurface: Surface
|
||||
|
||||
/**
|
||||
* onStopRecord нужен, чтобы ViewModel узнала о завершении записи
|
||||
*/
|
||||
|
||||
var onStopRecord = {}
|
||||
|
||||
/**
|
||||
* Инициализация энкодера
|
||||
* Кодек и частота берется из констант
|
||||
*/
|
||||
init {
|
||||
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1080, 1920)
|
||||
val format = MediaFormat.createVideoFormat(MEDIA_CODEC, 1080, 1920)
|
||||
format.setInteger(
|
||||
MediaFormat.KEY_COLOR_FORMAT,
|
||||
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
|
||||
)
|
||||
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30)
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1)
|
||||
mediaEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC)
|
||||
format.setInteger(MediaFormat.KEY_FRAME_RATE, MEDIA_FRAME_RATE)
|
||||
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
|
||||
mediaEncoder = MediaCodec.createEncoderByType(MEDIA_CODEC)
|
||||
mediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
|
||||
inputSurface = mediaEncoder.createInputSurface()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Запись на диск осуществляем с помощью MediaMuxer
|
||||
*/
|
||||
private lateinit var mediaMuxer: MediaMuxer
|
||||
private var muxerTrackIndex = -1
|
||||
@Volatile var isRunning = false
|
||||
|
||||
/**
|
||||
* Запуск рендера
|
||||
*/
|
||||
fun startRender() {
|
||||
|
||||
muxerTrackIndex = -1
|
||||
@@ -53,12 +68,17 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
|
||||
encode()
|
||||
|
||||
}
|
||||
/**
|
||||
* Это нужно для остановки энкодера после записи 120 фреймов
|
||||
*/
|
||||
helloSurface.setOnLastFrameRecordedListener {
|
||||
Log.d("Encoder", "last frame")
|
||||
isRunning = false
|
||||
onStopRecord()
|
||||
|
||||
}
|
||||
/**
|
||||
* Запуск отрисовки 120 фреймов в inputSurface энкодера
|
||||
*/
|
||||
helloSurface.setExternalSurface(inputSurface)
|
||||
helloSurface.startRecording(120)
|
||||
}
|
||||
@@ -69,37 +89,41 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
|
||||
when(encoderStatus) {
|
||||
MediaCodec.INFO_TRY_AGAIN_LATER -> { }
|
||||
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
|
||||
Log.d("encoder", "start muxer $muxerTrackIndex")
|
||||
val newFormat: MediaFormat = mediaEncoder.outputFormat
|
||||
muxerTrackIndex = mediaMuxer.addTrack(newFormat)
|
||||
mediaMuxer.start()
|
||||
}
|
||||
-3 -> {
|
||||
Log.d("encoder", "bad status $encoderStatus")
|
||||
}
|
||||
else -> {
|
||||
|
||||
/**
|
||||
* если статус >=0, значит что то есть в буфере
|
||||
*/
|
||||
val data = mediaEncoder.getOutputBuffer(encoderStatus)
|
||||
data?.let {
|
||||
data.position(bufferInfo.offset)
|
||||
data.limit(bufferInfo.offset + bufferInfo.size)
|
||||
it.position(bufferInfo.offset)
|
||||
it.limit(bufferInfo.offset + bufferInfo.size)
|
||||
mediaMuxer.writeSampleData(muxerTrackIndex, data, bufferInfo)
|
||||
}
|
||||
mediaEncoder.releaseOutputBuffer(encoderStatus, false)
|
||||
}
|
||||
}
|
||||
}
|
||||
Log.d("encoder","stop encoding")
|
||||
|
||||
mediaMuxer.stop()
|
||||
mediaMuxer.release()
|
||||
mediaEncoder.stop()
|
||||
|
||||
}
|
||||
private fun prepareFilePath(appPath: File): String {
|
||||
val fullPath = appPath.absolutePath+"/$MEDIA_FOLDER/"
|
||||
|
||||
/**
|
||||
* Метод возвращает полный путь с именем файла для записи видео
|
||||
* Здесь создается каталог test, если не существует
|
||||
*/
|
||||
private fun prepareFilePath(path: File): String {
|
||||
val fullPath = path.absolutePath+"/$MEDIA_FOLDER/"
|
||||
if (!File(fullPath).exists()) {
|
||||
if (!File(fullPath).mkdir()) {
|
||||
Log.d("main", "directory test not created")
|
||||
throw Exception("The test directory does not exist and has not been created:\n$fullPath$\n")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,51 @@
|
||||
package su.rst10h.inspiry.ui
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.widget.Button
|
||||
import androidx.activity.viewModels
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import su.rst10h.inspiry.MEDIA_FILE_NAME
|
||||
import su.rst10h.inspiry.MEDIA_FOLDER
|
||||
import su.rst10h.inspiry.R
|
||||
import androidx.core.content.FileProvider
|
||||
import su.rst10h.inspiry.*
|
||||
import su.rst10h.inspiry.data.ActivityState
|
||||
import su.rst10h.loopedworld.HelloSurface
|
||||
import java.io.File
|
||||
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
private val viewModel : MainViewModel by viewModels()
|
||||
private lateinit var helloSurface: HelloSurface
|
||||
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
//Это нужно на некоторых версиях MIUI для принудительного отключения ночной темы:
|
||||
//На обычных Android устройствах можно обойтись без нее
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
|
||||
setContentView(R.layout.activity_main)
|
||||
|
||||
val viewModel : MainViewModel by viewModels()
|
||||
/**
|
||||
* Так как мы пишем в папку приложения, разрешения можно было не запрашивать
|
||||
* но я не помню, прокатит ли это на Android 7 и ниже, просто на всякий случай:
|
||||
*/
|
||||
PermissionsChecker.verifyStoragePermissions(this)
|
||||
|
||||
/**
|
||||
* Binding не использовал, он тут роли не сыграет.
|
||||
*/
|
||||
val btn = findViewById<Button>(R.id.button)
|
||||
val surfaceView = findViewById<HelloSurface>(R.id.testView)
|
||||
|
||||
helloSurface = findViewById<HelloSurface>(R.id.testView)
|
||||
|
||||
helloSurface.frameRate = MEDIA_FRAME_RATE //Частоту обновления берем из констант
|
||||
|
||||
/**
|
||||
* Наблюдатель за состоянием и действия при изменении состояния:
|
||||
*/
|
||||
viewModel.state.observe(this, { state ->
|
||||
when (state) {
|
||||
ActivityState.WAIT -> {
|
||||
@@ -43,25 +62,61 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
ActivityState.OPEN_MEDIA -> {
|
||||
openMedia()
|
||||
viewModel.reset_state()
|
||||
viewModel.resetState()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
getExternalFilesDir(null)?.let {
|
||||
viewModel.initRender(surfaceView, it)
|
||||
/**
|
||||
* Инициализация энкодера через ViewModel
|
||||
* Конечно, не лучшее решение передавать View в Repository
|
||||
* Но там он нужен для решения текущей задачи
|
||||
* Тем более HelloSurface специально создан для этих целей
|
||||
*
|
||||
*/
|
||||
|
||||
getBasePath()?.let {
|
||||
viewModel.initRender(helloSurface, it)
|
||||
}
|
||||
|
||||
/**
|
||||
* Одна кнопка на все действия
|
||||
*/
|
||||
btn.setOnClickListener {
|
||||
viewModel.action()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Метод открывает заранее созданный файл
|
||||
* Можно было сделать чуть проще, но на устройствах с Android 11 тогда могут быть проблемы
|
||||
* В гугле такие затейники!
|
||||
*/
|
||||
private fun openMedia() {
|
||||
|
||||
val f = File(getBasePath(), "/$MEDIA_FOLDER/$MEDIA_FILE_NAME")
|
||||
val fileUri = FileProvider.getUriForFile( this, "${application.packageName}.fileprovider", f)
|
||||
val intent = Intent(Intent.ACTION_VIEW)
|
||||
intent.setDataAndType(Uri.parse(
|
||||
getExternalFilesDir(null)?.absolutePath+
|
||||
"/$MEDIA_FOLDER/$MEDIA_FILE_NAME"),
|
||||
"video/mp4")
|
||||
intent.setDataAndType(fileUri, "video/*")
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Повторная инициализация енкодера
|
||||
*/
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
getBasePath()?.let {
|
||||
viewModel.initRender(helloSurface, it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return app folder
|
||||
*/
|
||||
private fun getBasePath(): File? {
|
||||
|
||||
return this.getExternalFilesDir(null)
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,13 @@ import su.rst10h.inspiry.repository.VideoEncoder
|
||||
import su.rst10h.loopedworld.HelloSurface
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* View Model для главной и единственной Activity
|
||||
*/
|
||||
class MainViewModel: ViewModel() {
|
||||
/**
|
||||
* MutableLiveData содержит состояние приложения
|
||||
*/
|
||||
val state by lazy {
|
||||
MutableLiveData<ActivityState>().apply {
|
||||
this.value = WAIT
|
||||
@@ -17,14 +23,23 @@ class MainViewModel: ViewModel() {
|
||||
|
||||
private lateinit var videoEncoder: VideoEncoder
|
||||
|
||||
/**
|
||||
* Инициализация энкодера
|
||||
*/
|
||||
fun initRender(surface: HelloSurface, appPath: File) {
|
||||
videoEncoder = VideoEncoder(appPath, surface)
|
||||
}
|
||||
|
||||
fun reset_state() {
|
||||
/**
|
||||
* Сброс состояния
|
||||
*/
|
||||
fun resetState() {
|
||||
state.postValue(WAIT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Действия при нажатии кнопки и смена состояний
|
||||
*/
|
||||
fun action() {
|
||||
when (state.value) {
|
||||
WAIT -> {
|
||||
@@ -37,8 +52,6 @@ class MainViewModel: ViewModel() {
|
||||
RENDERED -> {
|
||||
state.postValue(OPEN_MEDIA)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
android:layout_marginBottom="100dp"
|
||||
android:elevation="8dp"
|
||||
app:frameNumber="0"
|
||||
app:frameRate="30"
|
||||
app:helloString="Hello World"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
@@ -23,7 +24,7 @@
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:layout_constraintVertical_bias="1.0"
|
||||
app:textSize="40sp" />
|
||||
/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/button"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<resources xmlns:tools="http://schemas.android.com/tools">
|
||||
<!-- Base application theme. -->
|
||||
<style name="Theme.Inspiry" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
|
||||
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
|
||||
<!-- Primary brand color. -->
|
||||
<item name="colorPrimary">@color/purple_200</item>
|
||||
<item name="colorPrimaryVariant">@color/purple_700</item>
|
||||
|
||||
5
app/src/main/res/xml/filepaths.xml
Normal file
5
app/src/main/res/xml/filepaths.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<paths>
|
||||
<files-path path="test/" name="files" />
|
||||
<external-path path="." name="external"/>
|
||||
</paths>
|
||||
Reference in New Issue
Block a user