mobile: keep screen on while playing/recording media (#3317)

* android: keep screen on while playing/recording media

* ios: keep screen on while playing/recording media

* different implementation on ios

* Revert "android: keep screen on while playing/recording media"

This reverts commit d291f006e9.

* different implementation on android

* refactor

---------

Co-authored-by: Avently <avently@local>
Co-authored-by: Evgeny Poberezkin <2769109+epoberezkin@users.noreply.github.com>
This commit is contained in:
Stanislav Dmitrenko 2023-11-08 00:56:38 +08:00 committed by GitHub
parent 3e46c5dfaf
commit 2dc621a56c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 43 additions and 3 deletions

View File

@ -120,6 +120,10 @@ class AppDelegate: NSObject, UIApplicationDelegate {
BGManager.shared.receiveMessages(complete) BGManager.shared.receiveMessages(complete)
} }
static func keepScreenOn(_ on: Bool) {
UIApplication.shared.isIdleTimerDisabled = on
}
} }
class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate { class SceneDelegate: NSObject, ObservableObject, UIWindowSceneDelegate {

View File

@ -46,6 +46,7 @@ class AudioRecorder {
audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH) audioRecorder?.record(forDuration: MAX_VOICE_MESSAGE_LENGTH)
await MainActor.run { await MainActor.run {
AppDelegate.keepScreenOn(true)
recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in recordingTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
guard let time = self.audioRecorder?.currentTime else { return } guard let time = self.audioRecorder?.currentTime else { return }
self.onTimer?(time) self.onTimer?(time)
@ -57,6 +58,9 @@ class AudioRecorder {
} }
return nil return nil
} catch let error { } catch let error {
await MainActor.run {
AppDelegate.keepScreenOn(false)
}
logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)") logger.error("AudioRecorder startAudioRecording error \(error.localizedDescription)")
return .error(error.localizedDescription) return .error(error.localizedDescription)
} }
@ -71,6 +75,7 @@ class AudioRecorder {
timer.invalidate() timer.invalidate()
} }
recordingTimer = nil recordingTimer = nil
AppDelegate.keepScreenOn(false)
} }
private func checkPermission() async -> Bool { private func checkPermission() async -> Bool {
@ -121,6 +126,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in playbackTimer = Timer.scheduledTimer(withTimeInterval: 0.01, repeats: true) { timer in
if self.audioPlayer?.isPlaying ?? false { if self.audioPlayer?.isPlaying ?? false {
AppDelegate.keepScreenOn(true)
guard let time = self.audioPlayer?.currentTime else { return } guard let time = self.audioPlayer?.currentTime else { return }
self.onTimer?(time) self.onTimer?(time)
} }
@ -129,6 +135,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
func pause() { func pause() {
audioPlayer?.pause() audioPlayer?.pause()
AppDelegate.keepScreenOn(false)
} }
func play() { func play() {
@ -149,6 +156,7 @@ class AudioPlayer: NSObject, AVAudioPlayerDelegate {
func stop() { func stop() {
if let player = audioPlayer { if let player = audioPlayer {
player.stop() player.stop()
AppDelegate.keepScreenOn(false)
} }
audioPlayer = nil audioPlayer = nil
if let timer = playbackTimer { if let timer = playbackTimer {

View File

@ -39,6 +39,7 @@ struct ActiveCallView: View {
} }
.onAppear { .onAppear {
logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)") logger.debug("ActiveCallView: appear client is nil \(client == nil), scenePhase \(String(describing: scenePhase), privacy: .public), canConnectCall \(canConnectCall)")
AppDelegate.keepScreenOn(true)
createWebRTCClient() createWebRTCClient()
dismissAllSheets() dismissAllSheets()
} }
@ -48,6 +49,7 @@ struct ActiveCallView: View {
} }
.onDisappear { .onDisappear {
logger.debug("ActiveCallView: disappear") logger.debug("ActiveCallView: disappear")
AppDelegate.keepScreenOn(false)
client?.endCall() client?.endCall()
} }
.onChange(of: m.callCommand) { _ in sendCommandToClient()} .onChange(of: m.callCommand) { _ in sendCommandToClient()}

View File

@ -6,6 +6,7 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import AVKit import AVKit
import Combine
struct VideoPlayerView: UIViewRepresentable { struct VideoPlayerView: UIViewRepresentable {
@ -37,6 +38,9 @@ struct VideoPlayerView: UIViewRepresentable {
player.seek(to: CMTime.zero) player.seek(to: CMTime.zero)
player.play() player.play()
} }
context.coordinator.publisher = player.publisher(for: \.timeControlStatus).sink { status in
AppDelegate.keepScreenOn(status == .playing)
}
return controller.view return controller.view
} }
@ -50,11 +54,13 @@ struct VideoPlayerView: UIViewRepresentable {
class Coordinator: NSObject { class Coordinator: NSObject {
var controller: AVPlayerViewController? var controller: AVPlayerViewController?
var timeObserver: Any? = nil var timeObserver: Any? = nil
var publisher: AnyCancellable? = nil
deinit { deinit {
if let timeObserver = timeObserver { if let timeObserver = timeObserver {
NotificationCenter.default.removeObserver(timeObserver) NotificationCenter.default.removeObserver(timeObserver)
} }
publisher?.cancel()
} }
} }
} }

View File

@ -48,6 +48,7 @@ actual class RecorderNative: RecorderInterface {
recStartedAt = System.currentTimeMillis() recStartedAt = System.currentTimeMillis()
progressJob = CoroutineScope(Dispatchers.Default).launch { progressJob = CoroutineScope(Dispatchers.Default).launch {
while(isActive) { while(isActive) {
keepScreenOn(true)
onProgressUpdate(progress(), false) onProgressUpdate(progress(), false)
delay(50) delay(50)
} }
@ -84,6 +85,7 @@ actual class RecorderNative: RecorderInterface {
progressJob = null progressJob = null
filePath = null filePath = null
recorder = null recorder = null
keepScreenOn(false)
return (realDuration(path) ?: 0).also { recStartedAt = null } return (realDuration(path) ?: 0).also { recStartedAt = null }
} }
@ -170,6 +172,7 @@ actual object AudioPlayer: AudioPlayerInterface {
progressJob = CoroutineScope(Dispatchers.Default).launch { progressJob = CoroutineScope(Dispatchers.Default).launch {
onProgressUpdate(player.currentPosition, TrackState.PLAYING) onProgressUpdate(player.currentPosition, TrackState.PLAYING)
while(isActive && player.isPlaying) { while(isActive && player.isPlaying) {
keepScreenOn(true)
// Even when current position is equal to duration, the player has isPlaying == true for some time, // Even when current position is equal to duration, the player has isPlaying == true for some time,
// so help to make the playback stopped in UI immediately // so help to make the playback stopped in UI immediately
if (player.currentPosition == player.duration) { if (player.currentPosition == player.duration) {
@ -187,6 +190,7 @@ actual object AudioPlayer: AudioPlayerInterface {
if (isActive) { if (isActive) {
onProgressUpdate(player.duration, TrackState.PAUSED) onProgressUpdate(player.duration, TrackState.PAUSED)
} }
keepScreenOn(false)
onProgressUpdate(null, TrackState.PAUSED) onProgressUpdate(null, TrackState.PAUSED)
} }
return player.duration return player.duration
@ -196,6 +200,7 @@ actual object AudioPlayer: AudioPlayerInterface {
progressJob?.cancel() progressJob?.cancel()
progressJob = null progressJob = null
player.pause() player.pause()
keepScreenOn(false)
return player.currentPosition return player.currentPosition
} }
@ -203,6 +208,7 @@ actual object AudioPlayer: AudioPlayerInterface {
if (currentlyPlaying.value == null) return if (currentlyPlaying.value == null) return
player.stop() player.stop()
stopListener() stopListener()
keepScreenOn(false)
} }
override fun stop(item: ChatItem) = stop(item.file?.fileName) override fun stop(item: ChatItem) = stop(item.file?.fileName)
@ -263,6 +269,7 @@ actual object AudioPlayer: AudioPlayerInterface {
override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) { override fun pause(audioPlaying: MutableState<Boolean>, pro: MutableState<Int>) {
pro.value = pause() pro.value = pause()
audioPlaying.value = false audioPlaying.value = false
keepScreenOn(false)
} }
override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) { override fun seekTo(ms: Int, pro: MutableState<Int>, filePath: String?) {

View File

@ -1,13 +1,10 @@
package chat.simplex.common.platform package chat.simplex.common.platform
import android.media.MediaMetadataRetriever
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.MutableState import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import chat.simplex.common.helpers.toUri
import chat.simplex.common.views.helpers.* import chat.simplex.common.views.helpers.*
import chat.simplex.res.MR import chat.simplex.res.MR
import com.google.android.exoplayer2.* import com.google.android.exoplayer2.*
@ -134,6 +131,7 @@ actual class VideoPlayer actual constructor(
player.addListener(object: Player.Listener{ player.addListener(object: Player.Listener{
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
super.onIsPlayingChanged(isPlaying) super.onIsPlayingChanged(isPlaying)
keepScreenOn(isPlaying)
// Produce non-ideal transition from stopped to playing state while showing preview image in ChatView // Produce non-ideal transition from stopped to playing state while showing preview image in ChatView
// videoPlaying.value = isPlaying // videoPlaying.value = isPlaying
} }
@ -192,6 +190,7 @@ actual class VideoPlayer actual constructor(
override fun release(remove: Boolean) { override fun release(remove: Boolean) {
player.release() player.release()
keepScreenOn(false)
if (remove) { if (remove) {
VideoPlayerHolder.players.remove(uri to gallery) VideoPlayerHolder.players.remove(uri to gallery)
} }

View File

@ -196,12 +196,14 @@ actual fun ActiveCallView() {
chatModel.activeCallViewIsVisible.value = true chatModel.activeCallViewIsVisible.value = true
// After the first call, End command gets added to the list which prevents making another calls // After the first call, End command gets added to the list which prevents making another calls
chatModel.callCommand.removeAll { it is WCallCommand.End } chatModel.callCommand.removeAll { it is WCallCommand.End }
keepScreenOn(true)
onDispose { onDispose {
activity.volumeControlStream = prevVolumeControlStream activity.volumeControlStream = prevVolumeControlStream
// Unlock orientation // Unlock orientation
activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED activity.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
chatModel.activeCallViewIsVisible.value = false chatModel.activeCallViewIsVisible.value = false
chatModel.callCommand.clear() chatModel.callCommand.clear()
keepScreenOn(false)
} }
} }
} }

View File

@ -11,6 +11,7 @@ import android.text.Spanned
import android.text.SpannedString import android.text.SpannedString
import android.text.style.* import android.text.style.*
import android.util.Base64 import android.util.Base64
import android.view.WindowManager
import androidx.compose.ui.graphics.* import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
@ -43,6 +44,17 @@ fun Resources.getText(id: StringResource, vararg args: Any): CharSequence {
return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY) return HtmlCompat.fromHtml(formattedHtml, HtmlCompat.FROM_HTML_MODE_LEGACY)
} }
fun keepScreenOn(on: Boolean) {
val window = mainActivity.get()?.window ?: return
Handler(Looper.getMainLooper()).post {
if (on) {
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
}
}
}
actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString { actual fun escapedHtmlToAnnotatedString(text: String, density: Density): AnnotatedString {
return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density) return spannableStringToAnnotatedString(HtmlCompat.fromHtml(text, HtmlCompat.FROM_HTML_MODE_LEGACY), density)
} }