xuning

未检测到record

1 <component name="InspectionProjectProfileManager"> 1 <component name="InspectionProjectProfileManager">
2 <profile version="1.0"> 2 <profile version="1.0">
3 <option name="myName" value="Project Default" /> 3 <option name="myName" value="Project Default" />
  4 + <inspection_tool class="ComposePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
  5 + <option name="composableFile" value="true" />
  6 + </inspection_tool>
  7 + <inspection_tool class="ComposePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
  8 + <option name="composableFile" value="true" />
  9 + </inspection_tool>
  10 + <inspection_tool class="ComposePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
  11 + <option name="composableFile" value="true" />
  12 + </inspection_tool>
  13 + <inspection_tool class="ComposePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
  14 + <option name="composableFile" value="true" />
  15 + </inspection_tool>
  16 + <inspection_tool class="GlancePreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
  17 + <option name="composableFile" value="true" />
  18 + </inspection_tool>
  19 + <inspection_tool class="GlancePreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
  20 + <option name="composableFile" value="true" />
  21 + </inspection_tool>
  22 + <inspection_tool class="GlancePreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
  23 + <option name="composableFile" value="true" />
  24 + </inspection_tool>
  25 + <inspection_tool class="GlancePreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
  26 + <option name="composableFile" value="true" />
  27 + </inspection_tool>
4 <inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WEAK WARNING" enabled_by_default="true"> 28 <inspection_tool class="MemberVisibilityCanBePrivate" enabled="true" level="WEAK WARNING" enabled_by_default="true">
5 <scope name="Library Projects" level="WEAK WARNING" enabled="false" /> 29 <scope name="Library Projects" level="WEAK WARNING" enabled="false" />
6 </inspection_tool> 30 </inspection_tool>
@@ -12,6 +36,9 @@ @@ -12,6 +36,9 @@
12 <option name="composableFile" value="true" /> 36 <option name="composableFile" value="true" />
13 <option name="previewFile" value="true" /> 37 <option name="previewFile" value="true" />
14 </inspection_tool> 38 </inspection_tool>
  39 + <inspection_tool class="PreviewDeviceShouldUseNewSpec" enabled="true" level="WEAK WARNING" enabled_by_default="true">
  40 + <option name="composableFile" value="true" />
  41 + </inspection_tool>
15 <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true"> 42 <inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
16 <option name="composableFile" value="true" /> 43 <option name="composableFile" value="true" />
17 <option name="previewFile" value="true" /> 44 <option name="previewFile" value="true" />
@@ -36,6 +63,9 @@ @@ -36,6 +63,9 @@
36 <option name="composableFile" value="true" /> 63 <option name="composableFile" value="true" />
37 <option name="previewFile" value="true" /> 64 <option name="previewFile" value="true" />
38 </inspection_tool> 65 </inspection_tool>
  66 + <inspection_tool class="PreviewParameterProviderOnFirstParameter" enabled="true" level="ERROR" enabled_by_default="true">
  67 + <option name="composableFile" value="true" />
  68 + </inspection_tool>
39 <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true"> 69 <inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
40 <option name="composableFile" value="true" /> 70 <option name="composableFile" value="true" />
41 <option name="previewFile" value="true" /> 71 <option name="previewFile" value="true" />
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 package io.livekit.android.sample.record 17 package io.livekit.android.sample.record
18 18
  19 +import android.Manifest
19 import android.os.Bundle 20 import android.os.Bundle
20 import android.os.Environment 21 import android.os.Environment
21 import androidx.activity.ComponentActivity 22 import androidx.activity.ComponentActivity
@@ -30,59 +31,64 @@ import androidx.compose.runtime.getValue @@ -30,59 +31,64 @@ import androidx.compose.runtime.getValue
30 import androidx.compose.runtime.livedata.observeAsState 31 import androidx.compose.runtime.livedata.observeAsState
31 import androidx.compose.ui.Modifier 32 import androidx.compose.ui.Modifier
32 import androidx.lifecycle.MutableLiveData 33 import androidx.lifecycle.MutableLiveData
33 -import androidx.lifecycle.lifecycleScope  
34 -import io.livekit.android.AudioOptions  
35 -import io.livekit.android.LiveKit  
36 -import io.livekit.android.LiveKitOverrides  
37 -import io.livekit.android.room.Room  
38 -import io.livekit.android.room.track.LocalVideoTrack  
39 -import io.livekit.android.room.track.Track 34 +import androidx.core.app.ActivityCompat
40 import io.livekit.android.sample.record.ui.theme.LivekitandroidTheme 35 import io.livekit.android.sample.record.ui.theme.LivekitandroidTheme
41 import io.livekit.android.sample.util.requestNeededPermissions 36 import io.livekit.android.sample.util.requestNeededPermissions
42 -import kotlinx.coroutines.launch 37 +import livekit.org.webrtc.AudioSource
  38 +import livekit.org.webrtc.AudioTrack
  39 +import livekit.org.webrtc.Camera2Enumerator
  40 +import livekit.org.webrtc.CameraVideoCapturer
  41 +import livekit.org.webrtc.DefaultVideoDecoderFactory
  42 +import livekit.org.webrtc.DefaultVideoEncoderFactory
43 import livekit.org.webrtc.EglBase 43 import livekit.org.webrtc.EglBase
  44 +import livekit.org.webrtc.audio.JavaAudioDeviceModule
  45 +import livekit.org.webrtc.MediaConstraints
  46 +import livekit.org.webrtc.PeerConnectionFactory
  47 +import livekit.org.webrtc.SurfaceTextureHelper
  48 +import livekit.org.webrtc.VideoSource
  49 +import livekit.org.webrtc.VideoTrack
44 import java.io.File 50 import java.io.File
45 import java.io.IOException 51 import java.io.IOException
46 import java.util.Date 52 import java.util.Date
47 53
48 class MainActivity : ComponentActivity() { 54 class MainActivity : ComponentActivity() {
49 - lateinit var room: Room  
50 - var videoFileRenderer: VideoFileRenderer? = null  
51 - val connected = MutableLiveData(false) 55 + // 录制状态
  56 + private val isRecording = MutableLiveData(false)
  57 +
  58 + // WebRTC / 采集组件
  59 + private var eglBase: EglBase? = null
  60 + private var factory: PeerConnectionFactory? = null
  61 + private var videoCapturer: CameraVideoCapturer? = null
  62 + private var surfaceTextureHelper: SurfaceTextureHelper? = null
  63 + private var videoSource: VideoSource? = null
  64 + private var videoTrack: VideoTrack? = null
  65 + private var audioSource: AudioSource? = null
  66 + private var audioTrack: AudioTrack? = null
  67 +
  68 + // 文件渲染器
  69 + private var videoFileRenderer: VideoFileRenderer? = null
52 70
53 override fun onCreate(savedInstanceState: Bundle?) { 71 override fun onCreate(savedInstanceState: Bundle?) {
54 super.onCreate(savedInstanceState) 72 super.onCreate(savedInstanceState)
55 73
56 - // Create Room object.  
57 - room = LiveKit.create(  
58 - appContext = applicationContext,  
59 - overrides = LiveKitOverrides(  
60 - audioOptions = AudioOptions(  
61 - javaAudioDeviceModuleCustomizer = { builder ->  
62 - // Receive audio samples  
63 - builder.setSamplesReadyCallback { samples ->  
64 - videoFileRenderer?.onWebRtcAudioRecordSamplesReady(samples)  
65 - }  
66 - }  
67 - ),  
68 - )  
69 - )  
70 -  
71 setContent { 74 setContent {
72 LivekitandroidTheme { 75 LivekitandroidTheme {
73 Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) { 76 Surface(modifier = Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
74 Column { 77 Column {
75 - val isConnected by connected.observeAsState(false)  
76 -  
77 - if (isConnected) {  
78 - Text(text = "Connected!")  
79 - Button(onClick = { disconnectRoom() }) {  
80 - Text("Disconnect") 78 + Text(text = "Local Recorder (record-local)")
  79 + // 明确标题
  80 + Text(text = "Local Recorder (record-local)")
  81 + val recording by isRecording.observeAsState(false)
  82 +
  83 + if (recording) {
  84 + Text(text = "Recording")
  85 + Button(onClick = { stopRecording() }) {
  86 + Text("停止录制")
81 } 87 }
82 } else { 88 } else {
83 - Text(text = "Not Connected.")  
84 - Button(onClick = { connectToRoom() }) {  
85 - Text("Connect") 89 + Text(text = "Idle")
  90 + Button(onClick = { startRecording() }) {
  91 + Text("开始录制")
86 } 92 }
87 } 93 }
88 } 94 }
@@ -93,48 +99,109 @@ class MainActivity : ComponentActivity() { @@ -93,48 +99,109 @@ class MainActivity : ComponentActivity() {
93 requestNeededPermissions() 99 requestNeededPermissions()
94 } 100 }
95 101
96 - private fun connectToRoom() {  
97 - val url = "https://livekittest-demo.xuedianyun.com/"  
98 - val token = ""  
99 -  
100 - lifecycleScope.launch {  
101 - // Connect to server.  
102 - room.connect(  
103 - url,  
104 - token,  
105 - )  
106 -  
107 - val localParticipant = room.localParticipant  
108 - localParticipant.setMicrophoneEnabled(true)  
109 - localParticipant.setCameraEnabled(true)  
110 -  
111 - // Create output file.  
112 - val dir = getExternalFilesDir(Environment.DIRECTORY_MOVIES)  
113 - val file = File(dir, "${Date().time}.mp4")  
114 - if (!file.createNewFile()) {  
115 - throw IOException()  
116 - } 102 + private fun startRecording(withAudio: Boolean = true) {
  103 + // 权限(如需动态申请)
  104 + ActivityCompat.requestPermissions(
  105 + this,
  106 + arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO),
  107 + 1001
  108 + )
117 109
118 - // Setup video recording  
119 - val videoFileRenderer = VideoFileRenderer(  
120 - file.absolutePath,  
121 - EglBase.create().eglBaseContext,  
122 - true  
123 - )  
124 - this@MainActivity.videoFileRenderer = videoFileRenderer 110 + // 创建输出文件
  111 + val dir = getExternalFilesDir(Environment.DIRECTORY_MOVIES)
  112 + val file = File(dir, "${Date().time}.mp4")
  113 + if (!file.createNewFile()) {
  114 + throw IOException()
  115 + }
  116 +
  117 + // Egl 与 Renderer
  118 + val egl = EglBase.create()
  119 + eglBase = egl
  120 + val renderer = VideoFileRenderer(
  121 + file.absolutePath,
  122 + egl.eglBaseContext,
  123 + withAudio
  124 + )
  125 + videoFileRenderer = renderer
125 126
126 - // Attach to local video track.  
127 - val track = localParticipant.getTrackPublication(Track.Source.CAMERA)?.track as LocalVideoTrack  
128 - track.addRenderer(videoFileRenderer) 127 + // 初始化 WebRTC
  128 + PeerConnectionFactory.initialize(
  129 + PeerConnectionFactory.InitializationOptions.builder(applicationContext).createInitializationOptions()
  130 + )
129 131
130 - connected.value = true 132 + val audioModule = JavaAudioDeviceModule.builder(applicationContext)
  133 + .setSamplesReadyCallback { samples ->
  134 + videoFileRenderer?.onWebRtcAudioRecordSamplesReady(samples)
  135 + }
  136 + .createAudioDeviceModule()
  137 +
  138 + val encoderFactory = DefaultVideoEncoderFactory(egl.eglBaseContext, true, true)
  139 + val decoderFactory = DefaultVideoDecoderFactory(egl.eglBaseContext)
  140 +
  141 + factory = PeerConnectionFactory.builder()
  142 + .setAudioDeviceModule(audioModule)
  143 + .setVideoEncoderFactory(encoderFactory)
  144 + .setVideoDecoderFactory(decoderFactory)
  145 + .createPeerConnectionFactory()
  146 +
  147 + // 视频采集
  148 + surfaceTextureHelper = SurfaceTextureHelper.create("CaptureThread", egl.eglBaseContext)
  149 + val enumerator = Camera2Enumerator(this)
  150 + val deviceName = enumerator.deviceNames.find { enumerator.isFrontFacing(it) } ?: enumerator.deviceNames.first()
  151 + videoCapturer = enumerator.createCapturer(deviceName, null)
  152 +
  153 + videoSource = factory?.createVideoSource(false)
  154 + videoCapturer?.initialize(surfaceTextureHelper, this, videoSource!!.capturerObserver)
  155 + videoCapturer?.startCapture(1280, 720, 30)
  156 +
  157 + videoTrack = factory?.createVideoTrack("LOCAL_VIDEO", videoSource)
  158 + // 将视频帧送入文件渲染器
  159 + videoTrack?.addSink(renderer)
  160 +
  161 + // 音频(可选)
  162 + if (withAudio) {
  163 + val audioConstraints = MediaConstraints()
  164 + audioSource = factory?.createAudioSource(audioConstraints)
  165 + audioTrack = factory?.createAudioTrack("LOCAL_AUDIO", audioSource)
  166 + audioTrack?.setEnabled(true)
131 } 167 }
  168 +
  169 + isRecording.value = true
132 } 170 }
133 171
134 - fun disconnectRoom() {  
135 - room.disconnect() 172 + fun stopRecording() {
  173 + // 停止视频采集
  174 + try {
  175 + videoCapturer?.stopCapture()
  176 + } catch (_: Exception) {}
  177 + videoCapturer?.dispose()
  178 + videoCapturer = null
  179 +
  180 + videoTrack?.dispose()
  181 + videoTrack = null
  182 +
  183 + surfaceTextureHelper?.dispose()
  184 + surfaceTextureHelper = null
  185 +
  186 + videoSource?.dispose()
  187 + videoSource = null
  188 +
  189 + // 停止音频
  190 + audioTrack?.dispose()
  191 + audioTrack = null
  192 + audioSource?.dispose()
  193 + audioSource = null
  194 +
  195 + // 释放渲染器(写入并关闭 MP4)
136 videoFileRenderer?.release() 196 videoFileRenderer?.release()
137 videoFileRenderer = null 197 videoFileRenderer = null
138 - connected.value = false 198 +
  199 + // 释放工厂与 EGL
  200 + factory?.dispose()
  201 + factory = null
  202 + eglBase?.release()
  203 + eglBase = null
  204 +
  205 + isRecording.value = false
139 } 206 }
140 } 207 }