Toggle navigation
Toggle navigation
此项目
正在载入...
Sign in
xuning
/
livekitAndroidXuningTest
转到一个项目
Toggle navigation
项目
群组
代码片段
帮助
Toggle navigation pinning
Project
Activity
Repository
Pipelines
Graphs
Issues
0
Merge Requests
0
Wiki
Network
Create a new issue
Builds
Commits
Authored by
xuning
2025-10-13 13:03:24 +0800
Browse Files
Options
Browse Files
Download
Email Patches
Plain Diff
Commit
53f47776101a123949e6e8cc6785b4624126a708
53f47776
1 parent
33ec6071
未检测到record
隐藏空白字符变更
内嵌
并排对比
正在显示
2 个修改的文件
包含
167 行增加
和
70 行删除
.idea/inspectionProfiles/Project_Default.xml
sample-app-record-local/src/main/java/io/livekit/android/sample/record/MainActivity.kt
.idea/inspectionProfiles/Project_Default.xml
查看文件 @
53f4777
<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"
/>
...
...
sample-app-record-local/src/main/java/io/livekit/android/sample/record/MainActivity.kt
查看文件 @
53f4777
...
...
@@ -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
}
}
...
...
请
注册
或
登录
后发表评论