Skip to content

Commit

Permalink
Merge pull request #80 from shiguredo/feature/fix-screencast-b
Browse files Browse the repository at this point in the history
Pixel で1つのアプリをキャストした際の不自然な挙動を改善する
  • Loading branch information
zztkm authored Aug 23, 2024
2 parents aacfa27 + 857b0c1 commit fd6d96c
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 27 deletions.
15 changes: 15 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@
- [UPDATE] Kotlin のバージョンを 1.9.25 に上げる
- 合わせて、kotlinCompilerExtensionVersion を 1.5.15 に上げる
- @zztkm
- [UPDATE] スクリーンキャストサンプルで 1 つのアプリを選択して配信した際の挙動を改善する
- スクリーンキャストの映像を送信するために、MainActivity に画面更新を促す Intent を送る処理を追加
- スクリーンキャストサンプルは画面内に動きがなく、画面を動かすまで映像が送信されない問題があったため
- `Could not create virtual display` というエラーが出て 1 つのアプリでスクリーンキャストできない問題を修正
- SoraScreencastService を起動する Activity タスクを分けることで回避できることがわかったため、`ScreencastSetupActivity` の launchMode を `singleInstance` に変更した
- Activity のタスクを分けることに合わせて画面遷移の見直しを行った
- 動作確認は Android 14 の Pixel 端末でのみ行っており、他の端末での動作は未確認
- @tnoho
- [FIX] `Handler ()` で現在のスレッドに関連付けられた Looper を利用するようになっていたことで発生していた以下の問題を修正する
- 発生した問題
- スクリーンキャストが正常終了しなかった場合に、`SoraScreencastService.closeChannel()` の処理が main スレッド以外で実行されて `CalledFromWrongThreadException` が発生する
- `SoraMediaChannel.Listener.onClose()` の呼び出しにより `SoraScreencastService.closeChannel()` を実行するときに内部の `Handler()` 呼び出しがブロッキングされ、アプリが停止するケースがあった
- 修正内容
- `Handler (Looper looper)``getMainLooper()` で取得した、メインスレッドに関連付けられた Looper を利用するようにした
- @tnoho

## sora-andoroid-sdk-2024.2.0

Expand Down
20 changes: 12 additions & 8 deletions samples/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,23 +50,29 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ビデオチャット"
android:exported="true" />

<activity
<activity
android:name=".ui.SpotlightRoomSetupActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="スポットライト"
android:exported="true" />
<!--
ScreencastSetupActivity は Pixel で1つのアプリにこのサンプル自体を選んだ際に、
配信が失敗してしまう対策として android:launchMode="singleInstance" としている
またサンプルアプリ自体が選択候補に出るように android:taskAffinity を設定している
-->
<activity
android:name=".ui.ScreencastSetupActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="スクリーンキャスト"
android:exported="true" />
<activity
android:taskAffinity=".secondary"
android:launchMode="singleInstance"
android:exported="true" />
<activity
android:name=".ui.EffectedVideoChatSetupActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ビデオエフェクト"
android:exported="true" />
<activity
<activity
android:name=".ui.SimulcastSetupActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="サイマルキャスト"
Expand Down Expand Up @@ -97,13 +103,11 @@
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:label="ボイスチャット"
android:exported="true" />

<service
<service
android:name=".screencast.SoraScreencastService"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
android:enabled="true"
android:foregroundServiceType="mediaProjection" />

<activity
android:name=".ui.EffectedVideoChatActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import android.media.projection.MediaProjection
import android.os.Build
import android.os.Handler
import android.os.IBinder
import android.os.Looper
import android.view.LayoutInflater
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
Expand Down Expand Up @@ -56,6 +57,9 @@ class SoraScreencastService : Service() {

override fun onConnect(mediaChannel: SoraMediaChannel) {
SoraLogger.d(TAG, "[screencast] @onConnected")
// MainActivity に画面更新を促す Intent を送る
// 取れるイベントの中では最も遅いが、このタイミングでも送信が開始されているとは限らない
sendInvalidateBroadcast()
}

override fun onClose(mediaChannel: SoraMediaChannel) {
Expand Down Expand Up @@ -198,12 +202,25 @@ class SoraScreencastService : Service() {
}
}

private fun sendInvalidateBroadcast() {
// MainActivity に画面更新を促す Intent を送る
val intent = Intent("ACTION_INVALIDATE_VIEW")
intent.setPackage(applicationContext.packageName)
sendBroadcast(intent)
}

internal fun startCapturer() {
capturer?.let {
if (!capturing) {
capturing = true
SoraLogger.d(TAG, "startCapture")
val size = SoraScreenUtil.size(this)
/*
* Pixel シリーズにおいてキャスト時に 1つのアプリ でこのサンプルを選び、
* 下記の処理を呼び出した場合は失敗してしまう。
* それに対する対策として ScreencastSetupActivity を
* android:launchMode="singleInstance" にしている
*/
it.startCapture(
Math.round(size.x * req!!.videoScale),
Math.round(size.y * req!!.videoScale),
Expand All @@ -217,7 +234,7 @@ class SoraScreencastService : Service() {
capturer?.let {
if (capturing) {
capturing = false
SoraLogger.d(TAG, "startCapture")
SoraLogger.d(TAG, "stopCapture")
it.stopCapture()
}
}
Expand Down Expand Up @@ -280,7 +297,7 @@ class SoraScreencastService : Service() {
}

private fun closeChannel() {
val handler = Handler()
val handler = Handler(Looper.getMainLooper())
handler.post {
SoraLogger.d(TAG, "closeChannel")
mediaChannel?.disconnect()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,11 @@ class SoraScreencastServiceStarter(
)
intent.putExtra("SCREENCAST_REQUEST", request)
activity.startService(intent)
/*
* startService を行った Activity は1つのアプリにもなれないので、
* アプリ切り替えにも現れないよう finishAndRemoveTask でタスクごと消す
*/
activity.finishAndRemoveTask()
return true
}
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package jp.shiguredo.sora.sample.ui

import android.Manifest
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.snackbar.Snackbar
import jp.shiguredo.sora.sample.R
import jp.shiguredo.sora.sample.databinding.ActivityMainBinding
import jp.shiguredo.sora.sample.screencast.SoraScreencastService
import jp.shiguredo.sora.sdk.util.SoraLogger
import permissions.dispatcher.NeedsPermission
import permissions.dispatcher.OnPermissionDenied
Expand All @@ -27,6 +36,21 @@ class MainActivity : AppCompatActivity() {

private lateinit var binding: ActivityMainBinding

// スクリーンキャストが開始された通知を受け取る BroadcastReceiver
// スクリーンキャストを 1つのアプリ で開始した場合はこのアプリだと画面内に動きがないので映像が飛ばない
// 強制的に映像を飛ばすため SoraScreencastService より開始したことを示す Intent を送って invalidate を実行する
private val invalidateReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
// スクリーンキャストが実際に行われるまではタイムラグが発生しているので、
// あまりよくはないが 1000ms 後に invalidate を実行する
Handler(Looper.getMainLooper()).postDelayed({
// この関数で画面が再描画されスクリーンキャストに映る
window.decorView.rootView.invalidate()
}, 1000)
}
}

@SuppressLint("UnspecifiedRegisterReceiverFlag")
override fun onCreate(savedInstanceState: Bundle?) {

SoraLogger.enabled = true
Expand Down Expand Up @@ -80,6 +104,22 @@ class MainActivity : AppCompatActivity() {
binding.featureList.setHasFixedSize(true)
binding.featureList.layoutManager = llm
binding.featureList.adapter = adapter

// スクリーンキャストが開始された通知を受け取る BroadcastReceiver を登録
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(
invalidateReceiver, IntentFilter("ACTION_INVALIDATE_VIEW"),
RECEIVER_NOT_EXPORTED
)
} else {
registerReceiver(invalidateReceiver, IntentFilter("ACTION_INVALIDATE_VIEW"))
}
}

override fun onDestroy() {
super.onDestroy()
// スクリーンキャストが開始された通知を受け取る BroadcastReceiver を解除
unregisterReceiver(invalidateReceiver)
}

override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
Expand All @@ -105,6 +145,16 @@ class MainActivity : AppCompatActivity() {
@TargetApi(21)
@NeedsPermission(Manifest.permission.RECORD_AUDIO)
fun goToScreencastActivity() {
/*
* 1つのアプリ でこのサンプルを指定してスクリーンキャストを実行した場合、
* android:launchMode="singleInstance" で別タスクとした ScreencastSetupActivity は
* スクリーンキャストできないのでスクリーンキャスト実行中は遷移しないようにする
* 画面全体の場合や他のアプリを選択した場合は必要ないのに遷移しなくなるが許容する
*/
if (SoraScreencastService.isRunning()) {
Toast.makeText(this, "スクリーンキャストは実行中です", Toast.LENGTH_SHORT).show()
return
}
val intent = Intent(this, ScreencastSetupActivity::class.java)
startActivity(intent)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import android.annotation.TargetApi
import android.content.Intent
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import com.google.android.material.snackbar.Snackbar
import com.jaredrummler.materialspinner.MaterialSpinner
Expand Down Expand Up @@ -91,22 +90,10 @@ class ScreencastSetupActivity : AppCompatActivity() {
serviceClass = SoraScreencastService::class
)
screencastStarter?.start()
showNavigationMessage()
}

private fun showNavigationMessage() {
AlertDialog.Builder(this)
.setPositiveButton("OK") { _, _ -> goToHome() }
.setCancelable(false)
.setMessage("スクリーンキャストを終了するときは上のナビゲーションバーから終了ボタンを押してください。")
.show()
}

private fun goToHome() {
val intent = Intent(Intent.ACTION_MAIN)
intent.addCategory(Intent.CATEGORY_HOME)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
startActivity(intent)
/*
* 1つのアプリ をスクリーンキャスト時にはここに下に何か書いても、
* すでに選ばれたアプリに遷移しているためユーザーの視界に入ることはない
*/
}

private fun selectedItem(spinner: MaterialSpinner): String {
Expand Down

0 comments on commit fd6d96c

Please sign in to comment.