xuning

未检测到record

<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<scope name="Library Projects" level="WEAK WARNING" enabled="false" />
</inspection_tool>
... ... @@ -12,6 +36,9 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
... ... @@ -36,6 +63,9 @@
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
<option name="previewFile" value="true" />
... ...
... ... @@ -16,6 +16,7 @@
package io.livekit.android.sample.record
import android.Manifest
import android.os.Bundle
import android.os.Environment
import androidx.activity.ComponentActivity
... ... @@ -30,59 +31,64 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Modifier
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.lifecycleScope
import io.livekit.android.AudioOptions
import io.livekit.android.LiveKit
import io.livekit.android.LiveKitOverrides
import io.livekit.android.room.Room
import io.livekit.android.room.track.LocalVideoTrack
import io.livekit.android.room.track.Track
import androidx.core.app.ActivityCompat
import io.livekit.android.sample.record.ui.theme.LivekitandroidTheme
import io.livekit.android.sample.util.requestNeededPermissions
import kotlinx.coroutines.launch
import livekit.org.webrtc.AudioSource
import livekit.org.webrtc.AudioTrack
import livekit.org.webrtc.Camera2Enumerator
import livekit.org.webrtc.CameraVideoCapturer
import livekit.org.webrtc.DefaultVideoDecoderFactory
import livekit.org.webrtc.DefaultVideoEncoderFactory
import livekit.org.webrtc.EglBase
import livekit.org.webrtc.audio.JavaAudioDeviceModule
import livekit.org.webrtc.MediaConstraints
import livekit.org.webrtc.PeerConnectionFactory
import livekit.org.webrtc.SurfaceTextureHelper
import livekit.org.webrtc.VideoSource
import livekit.org.webrtc.VideoTrack
import java.io.File
import java.io.IOException
import java.util.Date
class MainActivity : ComponentActivity() {
lateinit var room: Room
var videoFileRenderer: VideoFileRenderer? = null
val connected = MutableLiveData(false)
// 录制状态
private val isRecording = MutableLiveData(false)
// WebRTC / 采集组件
private var eglBase: EglBase? = null
private var factory: PeerConnectionFactory? = null
private var videoCapturer: CameraVideoCapturer? = null
private var surfaceTextureHelper: SurfaceTextureHelper? = null
private var videoSource: VideoSource? = null
private var videoTrack: VideoTrack? = null
private var audioSource: AudioSource? = null
private var audioTrack: AudioTrack? = null
// 文件渲染器
private var videoFileRenderer: VideoFileRenderer? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Create Room object.
room = LiveKit.create(
appContext = applicationContext,
overrides = LiveKitOverrides(
audioOptions = AudioOptions(
javaAudioDeviceModuleCustomizer = { builder ->
// Receive audio samples
builder.setSamplesReadyCallback { samples ->
videoFileRenderer?.onWebRtcAudioRecordSamplesReady(samples)
}
}
),
)
)
setContent {
LivekitandroidTheme {
Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
Column {
val isConnected by connected.observeAsState(false)
if (isConnected) {
Text(text = "Connected!")
Button(onClick = { disconnectRoom() }) {
Text("Disconnect")
Text(text = "Local Recorder (record-local)")
// 明确标题
Text(text = "Local Recorder (record-local)")
val recording by isRecording.observeAsState(false)
if (recording) {
Text(text = "Recording")
Button(onClick = { stopRecording() }) {
Text("停止录制")
}
} else {
Text(text = "Not Connected.")
Button(onClick = { connectToRoom() }) {
Text("Connect")
Text(text = "Idle")
Button(onClick = { startRecording() }) {
Text("开始录制")
}
}
}
... ... @@ -93,48 +99,109 @@ class MainActivity : ComponentActivity() {
requestNeededPermissions()
}
private fun connectToRoom() {
val url = "https://livekittest-demo.xuedianyun.com/"
val token = ""
lifecycleScope.launch {
// Connect to server.
room.connect(
url,
token,
)
val localParticipant = room.localParticipant
localParticipant.setMicrophoneEnabled(true)
localParticipant.setCameraEnabled(true)
// Create output file.
val dir = getExternalFilesDir(Environment.DIRECTORY_MOVIES)
val file = File(dir, "${Date().time}.mp4")
if (!file.createNewFile()) {
throw IOException()
}
private fun startRecording(withAudio: Boolean = true) {
// 权限(如需动态申请)
ActivityCompat.requestPermissions(
this,
arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
1001
)
// Setup video recording
val videoFileRenderer = VideoFileRenderer(
file.absolutePath,
EglBase.create().eglBaseContext,
true
)
this@MainActivity.videoFileRenderer = videoFileRenderer
// 创建输出文件
val dir = getExternalFilesDir(Environment.DIRECTORY_MOVIES)
val file = File(dir, "${Date().time}.mp4")
if (!file.createNewFile()) {
throw IOException()
}
// Egl 与 Renderer
val egl = EglBase.create()
eglBase = egl
val renderer = VideoFileRenderer(
file.absolutePath,
egl.eglBaseContext,
withAudio
)
videoFileRenderer = renderer
// Attach to local video track.
val track = localParticipant.getTrackPublication(Track.Source.CAMERA)?.track as LocalVideoTrack
track.addRenderer(videoFileRenderer)
// 初始化 WebRTC
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(applicationContext).createInitializationOptions()
)
connected.value = true
val audioModule = JavaAudioDeviceModule.builder(applicationContext)
.setSamplesReadyCallback { samples ->
videoFileRenderer?.onWebRtcAudioRecordSamplesReady(samples)
}
.createAudioDeviceModule()
val encoderFactory = DefaultVideoEncoderFactory(egl.eglBaseContext, true, true)
val decoderFactory = DefaultVideoDecoderFactory(egl.eglBaseContext)
factory = PeerConnectionFactory.builder()
.setAudioDeviceModule(audioModule)
.setVideoEncoderFactory(encoderFactory)
.setVideoDecoderFactory(decoderFactory)
.createPeerConnectionFactory()
// 视频采集
surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", egl.eglBaseContext)
val enumerator = Camera2Enumerator(this)
val deviceName = enumerator.deviceNames.find { enumerator.isFrontFacing(it) } ?: enumerator.deviceNames.first()
videoCapturer = enumerator.createCapturer(deviceName, null)
videoSource = factory?.createVideoSource(false)
videoCapturer?.initialize(surfaceTextureHelper, this, videoSource!!.capturerObserver)
videoCapturer?.startCapture(1280, 720, 30)
videoTrack = factory?.createVideoTrack("LOCAL_VIDEO", videoSource)
// 将视频帧送入文件渲染器
videoTrack?.addSink(renderer)
// 音频(可选)
if (withAudio) {
val audioConstraints = MediaConstraints()
audioSource = factory?.createAudioSource(audioConstraints)
audioTrack = factory?.createAudioTrack("LOCAL_AUDIO", audioSource)
audioTrack?.setEnabled(true)
}
isRecording.value = true
}
fun disconnectRoom() {
room.disconnect()
fun stopRecording() {
// 停止视频采集
try {
videoCapturer?.stopCapture()
} catch (_: Exception) {}
videoCapturer?.dispose()
videoCapturer = null
videoTrack?.dispose()
videoTrack = null
surfaceTextureHelper?.dispose()
surfaceTextureHelper = null
videoSource?.dispose()
videoSource = null
// 停止音频
audioTrack?.dispose()
audioTrack = null
audioSource?.dispose()
audioSource = null
// 释放渲染器(写入并关闭 MP4)
videoFileRenderer?.release()
videoFileRenderer = null
connected.value = false
// 释放工厂与 EGL
factory?.dispose()
factory = null
eglBase?.release()
eglBase = null
isRecording.value = false
}
}
... ...