davidliu
Committed by GitHub

Dl/lk 356 android send explicit video layers (#30)

* send video layers when adding track

* handle portrait aspect ratio
@@ -22,6 +22,8 @@ import org.webrtc.RtpParameters @@ -22,6 +22,8 @@ import org.webrtc.RtpParameters
22 import org.webrtc.RtpTransceiver 22 import org.webrtc.RtpTransceiver
23 import javax.inject.Named 23 import javax.inject.Named
24 import kotlin.math.abs 24 import kotlin.math.abs
  25 +import kotlin.math.max
  26 +import kotlin.math.min
25 import kotlin.math.roundToInt 27 import kotlin.math.roundToInt
26 28
27 class LocalParticipant 29 class LocalParticipant
@@ -185,40 +187,14 @@ internal constructor( @@ -185,40 +187,14 @@ internal constructor(
185 ), 187 ),
186 publishListener: PublishListener? = null 188 publishListener: PublishListener? = null
187 ) { 189 ) {
188 - if (localTrackPublications.any { it.track == track }) {  
189 - publishListener?.onPublishFailure(TrackException.PublishException("Track has already been published"))  
190 - return  
191 - }  
192 -  
193 - val cid = track.rtcTrack.id()  
194 - val builder = LivekitRtc.AddTrackRequest.newBuilder().apply { 190 + publishTrackImpl(
  191 + track,
  192 + requestConfig = {
195 disableDtx = !options.dtx 193 disableDtx = !options.dtx
196 source = LivekitModels.TrackSource.MICROPHONE 194 source = LivekitModels.TrackSource.MICROPHONE
197 - }  
198 - val trackInfo = engine.addTrack(  
199 - cid = cid,  
200 - name = track.name,  
201 - kind = track.kind.toProto(),  
202 - builder = builder 195 + },
  196 + publishListener = publishListener,
203 ) 197 )
204 - val transInit = RtpTransceiver.RtpTransceiverInit(  
205 - RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,  
206 - listOf(this.sid)  
207 - )  
208 - // TODO: sendEncodings to customize  
209 - val transceiver = engine.publisher.peerConnection.addTransceiver(track.rtcTrack, transInit)  
210 - track.transceiver = transceiver  
211 -  
212 - if (transceiver == null) {  
213 - publishListener?.onPublishFailure(TrackException.PublishException("null sender returned from peer connection"))  
214 - return  
215 - }  
216 -  
217 - val publication = LocalTrackPublication(trackInfo, track, this)  
218 - addTrackPublication(publication)  
219 - publishListener?.onPublishSuccess(publication)  
220 - internalListener?.onTrackPublished(publication, this)  
221 - eventBus.postEvent(ParticipantEvent.LocalTrackPublished(this, publication), scope)  
222 } 198 }
223 199
224 suspend fun publishVideoTrack( 200 suspend fun publishVideoTrack(
@@ -226,35 +202,63 @@ internal constructor( @@ -226,35 +202,63 @@ internal constructor(
226 options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults), 202 options: VideoTrackPublishOptions = VideoTrackPublishOptions(null, videoTrackPublishDefaults),
227 publishListener: PublishListener? = null 203 publishListener: PublishListener? = null
228 ) { 204 ) {
229 - if (localTrackPublications.any { it.track == track }) {  
230 - publishListener?.onPublishFailure(TrackException.PublishException("Track has already been published"))  
231 - return  
232 - }  
233 205
234 - val cid = track.rtcTrack.id()  
235 - val builder = LivekitRtc.AddTrackRequest.newBuilder().apply { 206 + val encodings = computeVideoEncodings(track.dimensions, options)
  207 + val videoLayers = videoLayersFromEncodings(track.dimensions.width, track.dimensions.height, encodings)
  208 +
  209 + publishTrackImpl(
  210 + track,
  211 + requestConfig = {
236 width = track.dimensions.width 212 width = track.dimensions.width
237 height = track.dimensions.height 213 height = track.dimensions.height
238 - source = if(track.options.isScreencast){ 214 + source = if (track.options.isScreencast) {
239 LivekitModels.TrackSource.SCREEN_SHARE 215 LivekitModels.TrackSource.SCREEN_SHARE
240 } else { 216 } else {
241 LivekitModels.TrackSource.CAMERA 217 LivekitModels.TrackSource.CAMERA
242 } 218 }
  219 + addAllLayers(videoLayers)
  220 + },
  221 + encodings = encodings,
  222 + publishListener = publishListener
  223 + )
  224 + }
  225 +
  226 +
  227 + private suspend fun publishTrackImpl(
  228 + track: Track,
  229 + requestConfig: LivekitRtc.AddTrackRequest.Builder.() -> Unit,
  230 + encodings: List<RtpParameters.Encoding> = emptyList(),
  231 + publishListener: PublishListener? = null
  232 + ) {
  233 + if (localTrackPublications.any { it.track == track }) {
  234 + publishListener?.onPublishFailure(TrackException.PublishException("Track has already been published"))
  235 + return
  236 + }
  237 +
  238 + val cid = track.rtcTrack.id()
  239 + val builder = LivekitRtc.AddTrackRequest.newBuilder().apply {
  240 + this.requestConfig()
243 } 241 }
244 val trackInfo = engine.addTrack( 242 val trackInfo = engine.addTrack(
245 cid = cid, 243 cid = cid,
246 name = track.name, 244 name = track.name,
247 - kind = LivekitModels.TrackType.VIDEO, 245 + kind = track.kind.toProto(),
248 builder = builder 246 builder = builder
249 ) 247 )
250 - val encodings = computeVideoEncodings(track.dimensions, options)  
251 val transInit = RtpTransceiver.RtpTransceiverInit( 248 val transInit = RtpTransceiver.RtpTransceiverInit(
252 RtpTransceiver.RtpTransceiverDirection.SEND_ONLY, 249 RtpTransceiver.RtpTransceiverDirection.SEND_ONLY,
253 listOf(this.sid), 250 listOf(this.sid),
254 encodings 251 encodings
255 ) 252 )
256 val transceiver = engine.publisher.peerConnection.addTransceiver(track.rtcTrack, transInit) 253 val transceiver = engine.publisher.peerConnection.addTransceiver(track.rtcTrack, transInit)
257 - track.transceiver = transceiver 254 +
  255 + when (track) {
  256 + is LocalVideoTrack -> track.transceiver
  257 + is LocalAudioTrack -> track.transceiver
  258 + else -> {
  259 + throw IllegalArgumentException("Trying to publish a non local track of type ${track.javaClass}")
  260 + }
  261 + }
258 262
259 if (transceiver == null) { 263 if (transceiver == null) {
260 publishListener?.onPublishFailure(TrackException.PublishException("null sender returned from peer connection")) 264 publishListener?.onPublishFailure(TrackException.PublishException("null sender returned from peer connection"))
@@ -289,14 +293,14 @@ internal constructor( @@ -289,14 +293,14 @@ internal constructor(
289 293
290 val encodings = mutableListOf<RtpParameters.Encoding>() 294 val encodings = mutableListOf<RtpParameters.Encoding>()
291 if (simulcast) { 295 if (simulcast) {
292 - encodings.add(encoding.toRtpEncoding("f"))  
293 296
294 val presets = presetsForResolution(width, height) 297 val presets = presetsForResolution(width, height)
295 val midPreset = presets[1] 298 val midPreset = presets[1]
296 val lowPreset = presets[0] 299 val lowPreset = presets[0]
297 300
298 fun calculateScale(parameter: VideoCaptureParameter): Double { 301 fun calculateScale(parameter: VideoCaptureParameter): Double {
299 - return height / parameter.height.toDouble() 302 + val longestSize = max(width, height)
  303 + return longestSize / parameter.width.toDouble()
300 } 304 }
301 305
302 fun checkEvenDimensions(parameter: VideoCaptureParameter): Boolean { 306 fun checkEvenDimensions(parameter: VideoCaptureParameter): Boolean {
@@ -306,36 +310,32 @@ internal constructor( @@ -306,36 +310,32 @@ internal constructor(
306 return isEven(parameter.height * scale) && isEven(parameter.width * scale) 310 return isEven(parameter.height * scale) && isEven(parameter.width * scale)
307 } 311 }
308 312
  313 + fun addEncoding(videoEncoding: VideoEncoding, scale: Double) {
  314 + if(encodings.size >= VIDEO_RIDS.size) {
  315 + throw IllegalStateException("Attempting to add more encodings than we have rids for!")
  316 + }
  317 + val rid = VIDEO_RIDS[encodings.size]
  318 + encodings.add(videoEncoding.toRtpEncoding(rid, scale))
  319 + }
  320 +
309 // if resolution is high enough, we send both h and q res. 321 // if resolution is high enough, we send both h and q res.
310 // otherwise only send h 322 // otherwise only send h
311 - if (width >= 960) { 323 + val size = max(width, height)
  324 + if (size >= 960) {
312 val hasEvenDimensions = 325 val hasEvenDimensions =
313 checkEvenDimensions(midPreset.capture) && checkEvenDimensions(lowPreset.capture) 326 checkEvenDimensions(midPreset.capture) && checkEvenDimensions(lowPreset.capture)
314 val midScale = if (hasEvenDimensions) calculateScale(midPreset.capture) else 2.0 327 val midScale = if (hasEvenDimensions) calculateScale(midPreset.capture) else 2.0
315 val lowScale = if (hasEvenDimensions) calculateScale(lowPreset.capture) else 4.0 328 val lowScale = if (hasEvenDimensions) calculateScale(lowPreset.capture) else 4.0
316 329
317 - encodings.add(  
318 - midPreset.encoding.toRtpEncoding(  
319 - "h",  
320 - midScale  
321 - )  
322 - )  
323 - encodings.add(  
324 - lowPreset.encoding.toRtpEncoding(  
325 - "q",  
326 - lowScale  
327 - )  
328 - ) 330 + addEncoding(lowPreset.encoding, lowScale)
  331 + addEncoding(midPreset.encoding, midScale)
329 } else { 332 } else {
330 val hasEvenDimensions = checkEvenDimensions(lowPreset.capture) 333 val hasEvenDimensions = checkEvenDimensions(lowPreset.capture)
331 val lowScale = if (hasEvenDimensions) calculateScale(lowPreset.capture) else 2.0 334 val lowScale = if (hasEvenDimensions) calculateScale(lowPreset.capture) else 2.0
332 - encodings.add(  
333 - lowPreset.encoding.toRtpEncoding(  
334 - "h",  
335 - lowScale  
336 - )  
337 - ) 335 + addEncoding(lowPreset.encoding, lowScale)
338 } 336 }
  337 +
  338 + addEncoding(encoding, 1.0)
339 } else { 339 } else {
340 encodings.add(encoding.toRtpEncoding()) 340 encodings.add(encoding.toRtpEncoding())
341 } 341 }
@@ -345,19 +345,63 @@ internal constructor( @@ -345,19 +345,63 @@ internal constructor(
345 private fun determineAppropriateEncoding(width: Int, height: Int): VideoEncoding { 345 private fun determineAppropriateEncoding(width: Int, height: Int): VideoEncoding {
346 val presets = presetsForResolution(width, height) 346 val presets = presetsForResolution(width, height)
347 347
  348 + // presets assume width is longest size
  349 + val longestSize = max(width, height)
348 val preset = presets 350 val preset = presets
349 - .lastOrNull { width >= it.capture.width && height >= it.capture.height }  
350 - ?: presets.first() 351 + .firstOrNull { it.capture.width >= longestSize}
  352 + ?: presets.last()
351 353
352 return preset.encoding 354 return preset.encoding
353 } 355 }
354 356
355 private fun presetsForResolution(width: Int, height: Int): List<VideoPreset> { 357 private fun presetsForResolution(width: Int, height: Int): List<VideoPreset> {
356 - val aspectRatio = width.toFloat() / height  
357 - if (abs(aspectRatio - 16f / 9f) < abs(aspectRatio - 4f / 3f)) {  
358 - return PRESETS_16_9 358 + val longestSize = max(width, height)
  359 + val shortestSize = min(width, height)
  360 + val aspectRatio = longestSize.toFloat() / shortestSize
  361 + return if (abs(aspectRatio - 16f / 9f) < abs(aspectRatio - 4f / 3f)) {
  362 + PRESETS_16_9
  363 + } else {
  364 + PRESETS_4_3
  365 + }
  366 + }
  367 +
  368 + private fun videoLayersFromEncodings(
  369 + trackWidth: Int,
  370 + trackHeight: Int,
  371 + encodings: List<RtpParameters.Encoding>
  372 + ): List<LivekitModels.VideoLayer> {
  373 + return if (encodings.isEmpty()) {
  374 + listOf(
  375 + LivekitModels.VideoLayer.newBuilder().apply {
  376 + width = trackWidth
  377 + height = trackHeight
  378 + quality = LivekitModels.VideoQuality.HIGH
  379 + bitrate = 0
  380 + }.build()
  381 + )
359 } else { 382 } else {
360 - return PRESETS_4_3 383 + encodings.map {
  384 + val scale = it.scaleResolutionDownBy ?: 1.0
  385 + var videoQuality = videoQualityForRid(it.rid ?: "")
  386 + if (videoQuality == LivekitModels.VideoQuality.UNRECOGNIZED && encodings.size == 1) {
  387 + videoQuality = LivekitModels.VideoQuality.HIGH
  388 + }
  389 + LivekitModels.VideoLayer.newBuilder().apply {
  390 + width = (trackWidth * scale).roundToInt()
  391 + height = (trackHeight * scale).roundToInt()
  392 + quality = videoQuality
  393 + bitrate = it.maxBitrateBps ?: 0
  394 + }.build()
  395 + }
  396 + }
  397 + }
  398 +
  399 + private fun videoQualityForRid(rid: String): LivekitModels.VideoQuality {
  400 + return when (rid) {
  401 + "f" -> LivekitModels.VideoQuality.HIGH
  402 + "h" -> LivekitModels.VideoQuality.MEDIUM
  403 + "q" -> LivekitModels.VideoQuality.LOW
  404 + else -> LivekitModels.VideoQuality.UNRECOGNIZED
361 } 405 }
362 } 406 }
363 407
@@ -446,6 +490,8 @@ internal constructor( @@ -446,6 +490,8 @@ internal constructor(
446 } 490 }
447 491
448 companion object { 492 companion object {
  493 + private val VIDEO_RIDS = arrayOf("q", "h", "f")
  494 +
449 // Note: maintain order from smallest to biggest. 495 // Note: maintain order from smallest to biggest.
450 private val PRESETS_16_9 = listOf( 496 private val PRESETS_16_9 = listOf(
451 VideoPreset169.QVGA, 497 VideoPreset169.QVGA,