Comments added, code cleared

This commit is contained in:
2021-05-11 16:44:05 +03:00
parent e5c0287f51
commit 80391efc44
14 changed files with 239 additions and 234 deletions

View File

@@ -4,31 +4,36 @@ import android.content.Context
import android.graphics.* import android.graphics.*
import android.text.TextPaint import android.text.TextPaint
import android.util.AttributeSet import android.util.AttributeSet
import android.util.Log
import android.view.Surface import android.view.Surface
import android.view.SurfaceHolder import android.view.SurfaceHolder
import android.view.SurfaceView import android.view.SurfaceView
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlin.system.measureTimeMillis import kotlin.system.measureTimeMillis
/**
* Класс наследован от SurfaceView
* В нем отрисовывается проход фразы Hello World за 20 кадров
* в превью можно переключать кадры, меняя _frameNumber от 0 до 19
* В режиме редактирования для удобной отладки строка заполняется красным фоном
*
*/
class HelloSurface : SurfaceView, SurfaceHolder.Callback { class HelloSurface : SurfaceView, SurfaceHolder.Callback {
private var _helloWorldString: String = "" private var _helloWorldString: String = "" //Строка для отображения
private var _textSize: Float = 100f private var _frameNumber: Int = 7 // Инициализация номера фрейма, видно на превью
private var _frameNumber: Int = 7 private var _frameRate: Int = 30 //Частота кадров
private lateinit var textPaint: TextPaint private lateinit var textPaint: TextPaint
private lateinit var backgroundPaint: Paint private lateinit var backgroundPaint: Paint //Фон надписи в режиме редактирвоания
private var textWidth: Float = 0f private var textWidth: Float = 0f
private var textHeight: Float = 0f private var textHeight: Float = 0f
private var textPositionVertical = 0f
private var frameDelta = 0f
private var mainJob: Job? = null private var mainJob: Job? = null
private var persistentSurface: Surface? = null
private var persistentSurface: Surface? = null //Surface для рендера и сохранения видео
/** /**
* The text to draw * Текст для отрисовки
*/ */
var helloWorldString: String var helloWorldString: String
get() = _helloWorldString get() = _helloWorldString
@@ -38,22 +43,24 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
} }
/** /**
* font Size * Частота кадров
*/ */
var textSize: Float var frameRate: Int
get() = _textSize get() = _frameRate
set(value) { set(value) {
_textSize = value _frameRate = value
invalidateTextPaintAndMeasurements()
} }
var frameNumber: Int
/**
* Номер фрейма, используется только в режиме редактирования для превью
*/
var frameNumber: Int
get() = _frameNumber get() = _frameNumber
set(value) { set(value) {
_frameNumber = value % 20 _frameNumber = value % 20
invalidateTextPaintAndMeasurements() invalidateTextPaintAndMeasurements()
} }
init { init {
holder.addCallback(this) holder.addCallback(this)
} }
@@ -75,20 +82,14 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
} }
private fun attrsInit(attrs: AttributeSet?, defStyle: Int) { private fun attrsInit(attrs: AttributeSet?, defStyle: Int) {
// Load attributes
val a = context.obtainStyledAttributes( val a = context.obtainStyledAttributes(
attrs, R.styleable.TestView, defStyle, 0 attrs, R.styleable.TestView, defStyle, 0
) )
_helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String" _helloWorldString = a.getString(R.styleable.TestView_helloString) ?: "Test String"
_textSize = a.getDimension( _frameRate = a.getInteger(R.styleable.TestView_frameRate, 30)
R.styleable.TestView_textSize, _frameNumber = a.getInteger(R.styleable.TestView_frameNumber, frameNumber)
textSize
)
_frameNumber = a.getInteger(
R.styleable.TestView_frameNumber,
frameNumber
)
a.recycle() a.recycle()
textPaint = TextPaint().apply { textPaint = TextPaint().apply {
@@ -103,17 +104,20 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
invalidateTextPaintAndMeasurements() invalidateTextPaintAndMeasurements()
} }
fun setExternalSurface(surface: Surface) { fun setExternalSurface(surface: Surface) {
persistentSurface = surface persistentSurface = surface
} }
/**
* Перегруженный onDraw остался только для превью
*/
override fun onDraw(canvas: Canvas?) { override fun onDraw(canvas: Canvas?) {
super.onDraw(canvas) super.onDraw(canvas)
if (isInEditMode) if (isInEditMode)
canvas?.let { canvas?.let {
drawHello(canvas, _frameNumber) drawHello(canvas, _frameNumber)
} }
} }
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec) super.onMeasure(widthMeasureSpec, heightMeasureSpec)
@@ -121,38 +125,46 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
} }
private fun invalidateTextPaintAndMeasurements() { private fun invalidateTextPaintAndMeasurements() {
val textBounds = Rect() 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 textHeight = textBounds.height()+0f
textWidth = textBounds.width()+0f textWidth = textBounds.width()+0f
} }
/**
* Выделение красным фоном надписи в превью
*/
private fun drawBounds(canvas: Canvas, sx: Int, sy: Int) { private fun drawBounds(canvas: Canvas, sx: Int, sy: Int) {
val textBounds = Rect() val textBounds = Rect()
textPaint.getTextBounds(helloWorldString, 0, helloWorldString.length, textBounds) textPaint.getTextBounds(_helloWorldString, 0, _helloWorldString.length, textBounds)
textBounds.right = textPaint.measureText(helloWorldString).toInt() textBounds.right = textPaint.measureText(_helloWorldString).toInt()
textBounds.offsetTo(sx, sy-textHeight.toInt()+textPaint.descent().toInt()) textBounds.offsetTo(sx, sy-textHeight.toInt()+textPaint.descent().toInt())
canvas.drawRect(textBounds, backgroundPaint) 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 val textSize = canvas.height/20f
textPaint.textSize = textSize textPaint.textSize = textSize
textWidth = textPaint.measureText(helloWorldString) textWidth = textPaint.measureText(_helloWorldString)
textPositionVertical = canvas.height/2f+textSize/2f val textPositionVertical = canvas.height/2f+textSize/2f
frameDelta = (canvas.width+textWidth)/20f 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) { private fun clearCanvas(canvas: Canvas) {
@@ -165,20 +177,31 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
renderLoop() renderLoop()
} }
} }
@Volatile private var maxFramesRender = 120 @Volatile private var maxFramesRender = 120
@Volatile private var framesRendered = -1 @Volatile private var framesRendered = -1
/**
* Метод, запускающий отсчет фреймов для записи
*/
fun startRecording(maxFrames: Int) { fun startRecording(maxFrames: Int) {
maxFramesRender = maxFrames maxFramesRender = maxFrames
framesRendered++ framesRendered++
} }
var onLastFrame : () -> Unit = @Synchronized {}
/**
* Метод, который будет вызван из корутины после рендера последнего фрейма для записи
* Он будет переназначен во время инициализации енкодера
*/
var onLastFrame : () -> Unit = {}
fun setOnLastFrameRecordedListener(listener: () -> Unit) { fun setOnLastFrameRecordedListener(listener: () -> Unit) {
onLastFrame = listener onLastFrame = listener
} }
/**
* Отрисовка превью и здесь же рендер фреймов
*/
private suspend fun renderLoop() { private suspend fun renderLoop() {
while(GlobalScope.isActive) { while(GlobalScope.isActive) {
val timeInMillis = measureTimeMillis { val timeInMillis = measureTimeMillis {
@@ -193,13 +216,15 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
preview() preview()
_frameNumber++ _frameNumber++
}
if (_frameNumber >= 20) _frameNumber = 0 if (_frameNumber >= 20) _frameNumber = 0
delay(33-timeInMillis) }
//Расчет паузы перед отрисовкой следующего фрейма
delay(1000/_frameRate-timeInMillis)
} }
} }
fun render() { //Рендер фрейма для записи видео
private fun render() {
persistentSurface?.let {surface -> persistentSurface?.let {surface ->
val pCanvas = surface.lockCanvas(null) val pCanvas = surface.lockCanvas(null)
pCanvas?.let { pCanvas?.let {
@@ -211,8 +236,8 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
} }
} }
} }
//Такой же рендер, но видимый на View и работающий постоянно
fun preview() { private fun preview() {
val canvas = holder.lockCanvas() val canvas = holder.lockCanvas()
canvas?.let { canvas?.let {
clearCanvas(it) clearCanvas(it)
@@ -224,7 +249,7 @@ class HelloSurface : SurfaceView, SurfaceHolder.Callback {
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) { override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
} }
//Остановка корутины при уничтожении surface
override fun surfaceDestroyed(holder: SurfaceHolder) { override fun surfaceDestroyed(holder: SurfaceHolder) {
runBlocking { runBlocking {
mainJob?.cancelAndJoin() mainJob?.cancelAndJoin()

View File

@@ -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)
}
}

View File

@@ -3,7 +3,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
<su.rst10h.loopedworld.TestView <su.rst10h.loopedworld.HelloSurface
style="@style/Widget.Theme.Inspiry.MyView" style="@style/Widget.Theme.Inspiry.MyView"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
@@ -11,6 +11,7 @@
android:paddingBottom="40dp" android:paddingBottom="40dp"
app:frameNumber="8" app:frameNumber="8"
app:helloString="Hello World" app:helloString="Hello World"
app:textSize="55sp" /> app:frameRate = "30"
/>
</FrameLayout> </FrameLayout>

View File

@@ -1,7 +1,7 @@
<resources> <resources>
<declare-styleable name="TestView"> <declare-styleable name="TestView">
<attr name="helloString" format="string" /> <attr name="helloString" format="string" />
<attr name="textSize" format="dimension" /> <attr name="frameRate" format="integer" />
<attr name="frameNumber" format="integer" /> <attr name="frameNumber" format="integer" />
</declare-styleable> </declare-styleable>
</resources> </resources>

View File

@@ -1,13 +1,11 @@
package su.rst10h.inspiry package su.rst10h.inspiry
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4 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.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith
import org.junit.Assert.*
/** /**
* Instrumented test, which will execute on an Android device. * Instrumented test, which will execute on an Android device.
* *

View File

@@ -12,12 +12,23 @@
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.Inspiry"> android:theme="@style/Theme.Inspiry">
<activity android:name=".ui.MainActivity"> <activity android:name=".ui.MainActivity"
android:configChanges="orientation"
android:screenOrientation="portrait">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </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> </application>
</manifest> </manifest>

View File

@@ -1,4 +1,11 @@
package su.rst10h.inspiry package su.rst10h.inspiry
import android.media.MediaFormat
/**
* Константы, доступные в любой части приложения.
*/
const val MEDIA_FOLDER = "test" const val MEDIA_FOLDER = "test"
const val MEDIA_FILE_NAME = "TestVideo.mp4" const val MEDIA_FILE_NAME = "TestVideo.mp4"
const val MEDIA_CODEC = MediaFormat.MIMETYPE_VIDEO_AVC
const val MEDIA_FRAME_RATE = 30

View File

@@ -5,6 +5,11 @@ import android.app.Activity
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
/**
* Проверка разрешений.
* Оставил на всякий случай
*/
class PermissionsChecker { class PermissionsChecker {
companion object { companion object {
private var REQUEST_EXTERNAL_STORAGE = 1 private var REQUEST_EXTERNAL_STORAGE = 1

View File

@@ -4,12 +4,13 @@ import android.media.MediaCodec
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.view.Surface import android.view.Surface
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import su.rst10h.inspiry.MEDIA_CODEC
import su.rst10h.inspiry.MEDIA_FILE_NAME import su.rst10h.inspiry.MEDIA_FILE_NAME
import su.rst10h.inspiry.MEDIA_FOLDER import su.rst10h.inspiry.MEDIA_FOLDER
import su.rst10h.inspiry.MEDIA_FRAME_RATE
import su.rst10h.loopedworld.HelloSurface import su.rst10h.loopedworld.HelloSurface
import java.io.File 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 bufferInfo = MediaCodec.BufferInfo()
private val mediaEncoder: MediaCodec private val mediaEncoder: MediaCodec
private lateinit var inputSurface: Surface private val inputSurface: Surface
/**
* onStopRecord нужен, чтобы ViewModel узнала о завершении записи
*/
var onStopRecord = {} var onStopRecord = {}
/**
* Инициализация энкодера
* Кодек и частота берется из констант
*/
init { init {
val format = MediaFormat.createVideoFormat(MediaFormat.MIMETYPE_VIDEO_AVC, 1080, 1920) val format = MediaFormat.createVideoFormat(MEDIA_CODEC, 1080, 1920)
format.setInteger( format.setInteger(
MediaFormat.KEY_COLOR_FORMAT, MediaFormat.KEY_COLOR_FORMAT,
MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface
) )
format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000) format.setInteger(MediaFormat.KEY_BIT_RATE, 2000000)
format.setInteger(MediaFormat.KEY_FRAME_RATE, 30) format.setInteger(MediaFormat.KEY_FRAME_RATE, MEDIA_FRAME_RATE)
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1) format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 5)
mediaEncoder = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC) mediaEncoder = MediaCodec.createEncoderByType(MEDIA_CODEC)
mediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE) mediaEncoder.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE)
inputSurface = mediaEncoder.createInputSurface() inputSurface = mediaEncoder.createInputSurface()
} }
/**
* Запись на диск осуществляем с помощью MediaMuxer
*/
private lateinit var mediaMuxer: MediaMuxer private lateinit var mediaMuxer: MediaMuxer
private var muxerTrackIndex = -1 private var muxerTrackIndex = -1
@Volatile var isRunning = false @Volatile var isRunning = false
/**
* Запуск рендера
*/
fun startRender() { fun startRender() {
muxerTrackIndex = -1 muxerTrackIndex = -1
@@ -53,12 +68,17 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
encode() encode()
} }
/**
* Это нужно для остановки энкодера после записи 120 фреймов
*/
helloSurface.setOnLastFrameRecordedListener { helloSurface.setOnLastFrameRecordedListener {
Log.d("Encoder", "last frame")
isRunning = false isRunning = false
onStopRecord() onStopRecord()
} }
/**
* Запуск отрисовки 120 фреймов в inputSurface энкодера
*/
helloSurface.setExternalSurface(inputSurface) helloSurface.setExternalSurface(inputSurface)
helloSurface.startRecording(120) helloSurface.startRecording(120)
} }
@@ -69,37 +89,41 @@ class VideoEncoder(private val baseFileDir: File, private val helloSurface: Hell
when(encoderStatus) { when(encoderStatus) {
MediaCodec.INFO_TRY_AGAIN_LATER -> { } MediaCodec.INFO_TRY_AGAIN_LATER -> { }
MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> { MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
Log.d("encoder", "start muxer $muxerTrackIndex")
val newFormat: MediaFormat = mediaEncoder.outputFormat val newFormat: MediaFormat = mediaEncoder.outputFormat
muxerTrackIndex = mediaMuxer.addTrack(newFormat) muxerTrackIndex = mediaMuxer.addTrack(newFormat)
mediaMuxer.start() mediaMuxer.start()
} }
-3 -> { -3 -> {
Log.d("encoder", "bad status $encoderStatus")
} }
else -> { else -> {
/**
* если статус >=0, значит что то есть в буфере
*/
val data = mediaEncoder.getOutputBuffer(encoderStatus) val data = mediaEncoder.getOutputBuffer(encoderStatus)
data?.let { data?.let {
data.position(bufferInfo.offset) it.position(bufferInfo.offset)
data.limit(bufferInfo.offset + bufferInfo.size) it.limit(bufferInfo.offset + bufferInfo.size)
mediaMuxer.writeSampleData(muxerTrackIndex, data, bufferInfo) mediaMuxer.writeSampleData(muxerTrackIndex, data, bufferInfo)
} }
mediaEncoder.releaseOutputBuffer(encoderStatus, false) mediaEncoder.releaseOutputBuffer(encoderStatus, false)
} }
} }
} }
Log.d("encoder","stop encoding")
mediaMuxer.stop() mediaMuxer.stop()
mediaMuxer.release() mediaMuxer.release()
mediaEncoder.stop() 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).exists()) {
if (!File(fullPath).mkdir()) { 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") throw Exception("The test directory does not exist and has not been created:\n$fullPath$\n")
} }
} }

View File

@@ -1,32 +1,51 @@
package su.rst10h.inspiry.ui package su.rst10h.inspiry.ui
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.widget.Button import android.widget.Button
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import su.rst10h.inspiry.MEDIA_FILE_NAME import androidx.core.content.FileProvider
import su.rst10h.inspiry.MEDIA_FOLDER import su.rst10h.inspiry.*
import su.rst10h.inspiry.R
import su.rst10h.inspiry.data.ActivityState import su.rst10h.inspiry.data.ActivityState
import su.rst10h.loopedworld.HelloSurface import su.rst10h.loopedworld.HelloSurface
import java.io.File
class MainActivity : AppCompatActivity() { class MainActivity : AppCompatActivity() {
private val viewModel : MainViewModel by viewModels()
private lateinit var helloSurface: HelloSurface
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
//Это нужно на некоторых версиях MIUI для принудительного отключения ночной темы:
//На обычных Android устройствах можно обойтись без нее
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO) AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
setContentView(R.layout.activity_main) setContentView(R.layout.activity_main)
val viewModel : MainViewModel by viewModels() /**
* Так как мы пишем в папку приложения, разрешения можно было не запрашивать
* но я не помню, прокатит ли это на Android 7 и ниже, просто на всякий случай:
*/
PermissionsChecker.verifyStoragePermissions(this)
/**
* Binding не использовал, он тут роли не сыграет.
*/
val btn = findViewById<Button>(R.id.button) 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 -> viewModel.state.observe(this, { state ->
when (state) { when (state) {
ActivityState.WAIT -> { ActivityState.WAIT -> {
@@ -43,25 +62,61 @@ class MainActivity : AppCompatActivity() {
} }
ActivityState.OPEN_MEDIA -> { ActivityState.OPEN_MEDIA -> {
openMedia() 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 { btn.setOnClickListener {
viewModel.action() viewModel.action()
} }
} }
/**
* Метод открывает заранее созданный файл
* Можно было сделать чуть проще, но на устройствах с Android 11 тогда могут быть проблемы
* В гугле такие затейники!
*/
private fun openMedia() { 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) val intent = Intent(Intent.ACTION_VIEW)
intent.setDataAndType(Uri.parse( intent.setDataAndType(fileUri, "video/*")
getExternalFilesDir(null)?.absolutePath+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
"/$MEDIA_FOLDER/$MEDIA_FILE_NAME"),
"video/mp4")
startActivity(intent) startActivity(intent)
} }
/**
* Повторная инициализация енкодера
*/
override fun onStart() {
super.onStart()
getBasePath()?.let {
viewModel.initRender(helloSurface, it)
}
}
/**
* @return app folder
*/
private fun getBasePath(): File? {
return this.getExternalFilesDir(null)
}
} }

View File

@@ -8,7 +8,13 @@ import su.rst10h.inspiry.repository.VideoEncoder
import su.rst10h.loopedworld.HelloSurface import su.rst10h.loopedworld.HelloSurface
import java.io.File import java.io.File
/**
* View Model для главной и единственной Activity
*/
class MainViewModel: ViewModel() { class MainViewModel: ViewModel() {
/**
* MutableLiveData содержит состояние приложения
*/
val state by lazy { val state by lazy {
MutableLiveData<ActivityState>().apply { MutableLiveData<ActivityState>().apply {
this.value = WAIT this.value = WAIT
@@ -17,14 +23,23 @@ class MainViewModel: ViewModel() {
private lateinit var videoEncoder: VideoEncoder private lateinit var videoEncoder: VideoEncoder
/**
* Инициализация энкодера
*/
fun initRender(surface: HelloSurface, appPath: File) { fun initRender(surface: HelloSurface, appPath: File) {
videoEncoder = VideoEncoder(appPath, surface) videoEncoder = VideoEncoder(appPath, surface)
} }
fun reset_state() { /**
* Сброс состояния
*/
fun resetState() {
state.postValue(WAIT) state.postValue(WAIT)
} }
/**
* Действия при нажатии кнопки и смена состояний
*/
fun action() { fun action() {
when (state.value) { when (state.value) {
WAIT -> { WAIT -> {
@@ -37,8 +52,6 @@ class MainViewModel: ViewModel() {
RENDERED -> { RENDERED -> {
state.postValue(OPEN_MEDIA) state.postValue(OPEN_MEDIA)
} }
} }
} }

View File

@@ -16,6 +16,7 @@
android:layout_marginBottom="100dp" android:layout_marginBottom="100dp"
android:elevation="8dp" android:elevation="8dp"
app:frameNumber="0" app:frameNumber="0"
app:frameRate="30"
app:helloString="Hello World" app:helloString="Hello World"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@@ -23,7 +24,7 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="1.0" app:layout_constraintVertical_bias="1.0"
app:textSize="40sp" /> />
<Button <Button
android:id="@+id/button" android:id="@+id/button"

View File

@@ -1,6 +1,7 @@
<resources xmlns:tools="http://schemas.android.com/tools"> <resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. --> <!-- Base application theme. -->
<style name="Theme.Inspiry" parent="Theme.MaterialComponents.DayNight.DarkActionBar"> <style name="Theme.Inspiry" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
<item name="android:forceDarkAllowed" tools:targetApi="q">false</item>
<!-- Primary brand color. --> <!-- Primary brand color. -->
<item name="colorPrimary">@color/purple_200</item> <item name="colorPrimary">@color/purple_200</item>
<item name="colorPrimaryVariant">@color/purple_700</item> <item name="colorPrimaryVariant">@color/purple_700</item>

View 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>