apps\hub\src\main\java\cn\nfarms\hub\ui\FfmpegScreen.kt¶
源码路径:
apps\hub\src\main\java\cn\nfarms\hub\ui\FfmpegScreen.kt· 137 行
package cn.nfarms.hub.ui
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import cn.nfarms.core.ui.BrandErr
import cn.nfarms.core.ui.BrandOk
import cn.nfarms.core.ui.BrandWarn
import cn.nfarms.core.ui.CardBox
import cn.nfarms.core.ui.KV
import cn.nfarms.core.ui.Pill
import cn.nfarms.core.ui.Section
import cn.nfarms.hub.HubUi
import cn.nfarms.hub.HubViewModel
/**
* 媒体页(ffmpeg)。
*
* 只做**播放器不做的事**:转码、抽帧、合流、抽音轨。播放走 Player 那个 APK 的 Media3 ——
* 这里再内置一套解码/播放没有意义,而且会让 APK 白白变大。
*/
@Composable
fun FfmpegScreen(ui: HubUi, vm: HubViewModel) {
var src by remember { mutableStateOf("") }
val found = ui.ffmpeg.path != null
Column(
Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(14.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
CardBox {
Row(verticalAlignment = Alignment.CenterVertically) {
Column(Modifier.weight(1f)) {
Section("FFmpeg", "只补 Media3 缺的格式:转码 / 抽帧 / 合流")
}
if (found) Pill("已找到", BrandOk) else Pill("未安装", BrandErr)
}
KV("可执行文件", ui.ffmpeg.path ?: "未找到", mono = true, valueColor = if (found) null else BrandErr)
KV("许可", if (ui.ffmpeg.gpl) "GPL" else "LGPL(默认)", valueColor = if (ui.ffmpeg.gpl) BrandErr else null)
Spacer(Modifier.height(6.dp))
Text(
"合规界限:默认构建是 LGPL,只提供 mpeg4 / aac 等 LGPL 可用编码器。" +
"要 libx264 必须换 GPL 构建 —— 那会把整个 APK 拖进 GPL,所以这里**不默认**提供," +
"要就得自己放一个 GPL 的 ffmpeg 并接受这个代价。",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.height(8.dp))
OutlinedButton(onClick = { vm.refreshFfmpeg() }) { Text("重新探测") }
}
CardBox {
Section("输入", "App 私有目录里的文件;分区存储下这里不申请存储权限")
OutlinedTextField(
value = src,
onValueChange = { src = it },
modifier = Modifier.fillMaxWidth(),
label = { Text("源文件绝对路径") },
singleLine = true,
textStyle = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace),
)
Spacer(Modifier.height(6.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
OutlinedButton(onClick = { src = vm.makeSampleSource() }) { Text("放个测试文件") }
}
}
CardBox {
Section("预设", "每个预设 = 一条 HUB 任务,能看进度、能取消")
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { vm.runPreset("remux", src) },
enabled = found && src.isNotBlank(),
) { Text("重封装") }
Button(
onClick = { vm.runPreset("audio", src) },
enabled = found && src.isNotBlank(),
) { Text("抽音轨") }
}
Spacer(Modifier.height(8.dp))
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Button(
onClick = { vm.runPreset("thumb", src) },
enabled = found && src.isNotBlank(),
) { Text("抽一帧") }
Button(
onClick = { vm.runPreset("transcode", src) },
enabled = found && src.isNotBlank(),
) { Text("转码 mpeg4") }
}
if (!found) {
Spacer(Modifier.height(8.dp))
Text(
"没找到 ffmpeg:把可执行文件放到 App 私有目录 bin/ffmpeg 并 chmod +x," +
"或把它放进 PATH。这里不内置二进制 —— 一个 60 MB 的二进制只为演示不值当。",
style = MaterialTheme.typography.bodySmall,
color = BrandWarn,
)
}
}
CardBox {
Section("为什么用 -progress 而不是解析 stderr", null)
Text(
"stderr 里的 frame=/time= 是给人看的,格式随版本变;" +
"-progress pipe:1 是稳定契约(out_time_us= / progress=end)," +
"解析它才不会因为换了个 ffmpeg 版本就把进度条弄坏。",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}