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:
parent
3e46c5dfaf
commit
2dc621a56c
@ -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 {
|
||||||
|
@ -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 {
|
||||||
|
@ -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()}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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?) {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user