/** * Copyright (C) 2021 Anthony Chomienne * This program is free software: you can redistribute it and/or modify it under the terms of the * GNU Affero General Public License as published by the Free Software Foundation, version 3. * * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. * See the GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License along with this program. * If not, see */ package fr.mobdev.peertubelive.activity import android.Manifest.permission import android.content.* import android.content.pm.PackageManager import android.hardware.SensorManager import android.net.Uri import android.os.Build import android.os.Bundle import android.provider.Settings import android.view.OrientationEventListener import android.view.SurfaceHolder import android.view.View import android.view.WindowManager import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.databinding.DataBindingUtil import com.pedro.encoder.input.video.CameraHelper import com.pedro.rtmp.utils.ConnectCheckerRtmp import com.pedro.rtplibrary.rtmp.RtmpCamera2 import fr.mobdev.peertubelive.R import fr.mobdev.peertubelive.databinding.StreamBinding import fr.mobdev.peertubelive.manager.InstanceManager.EXTRA_DATA import fr.mobdev.peertubelive.objects.StreamData import java.util.* import kotlin.collections.ArrayList class StreamActivity : AppCompatActivity() { private lateinit var binding: StreamBinding private lateinit var rtmpCamera2 : RtmpCamera2 private lateinit var streamData: StreamData private lateinit var orientationEventListener: OrientationEventListener private lateinit var lockReceiver: BroadcastReceiver private var surfaceInit: Boolean = false private var permissionGiven: Boolean = false private var streamIsActive: Boolean = false private var screenOrientation: Int = -1 private var lastScreenOrientation: Int = 0 private var rotationIsEnabled: Boolean = true private var orientationTimer: Timer = Timer() companion object { const val BACKGROUND :Int = 1 const val LOCK :Int = 2 const val BACK :Int = 3 const val STOP :Int = 4 const val NETWORK_ERROR :Int = 5 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) binding = DataBindingUtil.setContentView(this, R.layout.stream) orientationEventListener = object: OrientationEventListener(this, SensorManager.SENSOR_DELAY_NORMAL){ override fun onOrientationChanged(orientation: Int) { handlerOrientation(orientation) } } orientationEventListener.enable() streamData = intent.getParcelableExtra(EXTRA_DATA)!! binding.stop.setOnClickListener { askStopLive(STOP) } binding.switchCamera.setOnClickListener { rtmpCamera2.switchCamera() } binding.switchCamera.visibility = View.GONE binding.muteMicro.setOnClickListener { if (rtmpCamera2.isAudioMuted) { rtmpCamera2.enableAudio() binding.muteMicro.setImageResource(R.drawable.baseline_volume_up_24) } else { rtmpCamera2.disableAudio() binding.muteMicro.setImageResource(R.drawable.baseline_volume_off_24) } } binding.muteMicro.visibility = View.GONE binding.flash.setOnClickListener { if (rtmpCamera2.isLanternEnabled) { rtmpCamera2.disableLantern() binding.flash.setImageResource(R.drawable.baseline_flash_off_24) } else { rtmpCamera2.enableLantern() binding.flash.setImageResource(R.drawable.baseline_flash_on_24) } } binding.flash.visibility = View.GONE binding.rotation.setOnClickListener { if (rotationIsEnabled) binding.rotation.setImageResource(R.drawable.baseline_screen_lock_rotation_24) else binding.rotation.setImageResource(R.drawable.baseline_screen_rotation_24) rotationIsEnabled = !rotationIsEnabled } binding.rotation.visibility = View.GONE binding.surfaceView.holder.addCallback(object: SurfaceHolder.Callback { override fun surfaceCreated(p0: SurfaceHolder) { surfaceInit = true if (permissionGiven) startStream() } override fun surfaceChanged(p0: SurfaceHolder, p1: Int, p2: Int, p3: Int) { } override fun surfaceDestroyed(p0: SurfaceHolder) { if (this@StreamActivity::rtmpCamera2.isInitialized) { if (rtmpCamera2.isStreaming) { rtmpCamera2.stopStream() } rtmpCamera2.stopPreview() } } } ) lockReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { if (intent?.action.equals(Intent.ACTION_SCREEN_OFF)){ setResult(LOCK) finish() } else if (intent?.action.equals(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)){ val reason = intent?.getStringExtra("reason") if(reason.equals("homekey")){ setResult(BACKGROUND) finish() } } } } val filter = IntentFilter(Intent.ACTION_SCREEN_OFF) filter.addAction(Intent.ACTION_CLOSE_SYSTEM_DIALOGS) registerReceiver(lockReceiver, filter) } override fun onResume() { super.onResume() val permissions = getUnAllowedPermissions() if(permissions.isNotEmpty()) { var shouldShowRequest = true for(perm in permissions){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) shouldShowRequest = shouldShowRequest && shouldShowRequestPermissionRationale(perm) } if(shouldShowRequest) { binding.permissionInfo.visibility = View.VISIBLE binding.gotoPermission.visibility = View.VISIBLE binding.surfaceView.visibility = View.GONE binding.gotoPermission.setOnClickListener { val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS) intent.data = Uri.fromParts("package", packageName, null) startActivity(intent) } } else { ActivityCompat.requestPermissions(this, permissions.toTypedArray(), 1) } } else { binding.surfaceView.visibility = View.VISIBLE permissionGiven = true if(surfaceInit && !streamIsActive) startStream() } } override fun onStop() { super.onStop() if (this@StreamActivity::rtmpCamera2.isInitialized && !hasWindowFocus()) { unregisterReceiver(lockReceiver) setResult(BACKGROUND) finish() } } override fun onBackPressed() { askStopLive(BACK) } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults) var allPermissionGranted = true for (result in grantResults) { allPermissionGranted = allPermissionGranted && (result == PackageManager.PERMISSION_GRANTED) } if (allPermissionGranted) { permissionGiven = true if(surfaceInit && !streamIsActive) startStream() } else { binding.permissionInfo.visibility = View.VISIBLE binding.gotoPermission.visibility = View.VISIBLE binding.surfaceView.visibility = View.GONE } } private fun handlerOrientation(orientation: Int) { if(orientation < 0 || !rotationIsEnabled) { return } var localOrientation: Int localOrientation = when (orientation) { in 45..135 -> { 90 } in 135..225 -> { 180 } in 225..315 -> { 270 } else -> { 0 } } if(localOrientation != lastScreenOrientation) { lastScreenOrientation = localOrientation orientationTimer.cancel() orientationTimer.purge() orientationTimer = Timer() orientationTimer.schedule(object : TimerTask() { override fun run() { if (lastScreenOrientation != screenOrientation) { screenOrientation = lastScreenOrientation rtmpCamera2.glInterface.setStreamRotation(screenOrientation) if (screenOrientation == 90) { localOrientation = 270 } else if(screenOrientation == 270) { localOrientation = 90 } binding.flash.rotation = localOrientation.toFloat() binding.muteMicro.rotation = localOrientation.toFloat() binding.switchCamera.rotation = localOrientation.toFloat() binding.rotation.rotation = localOrientation.toFloat() } } },3000) } } private fun startStream() { streamIsActive = true binding.permissionInfo.visibility = View.GONE binding.gotoPermission.visibility = View.GONE binding.surfaceView.visibility = View.VISIBLE binding.muteMicro.visibility = View.VISIBLE binding.rotation.visibility = View.VISIBLE binding.switchCamera.visibility = View.VISIBLE binding.flash.visibility = View.VISIBLE val connectChecker : ConnectCheckerRtmp = object : ConnectCheckerRtmp { override fun onConnectionStartedRtmp(rtmpUrl: String) { } override fun onConnectionSuccessRtmp() { runOnUiThread { Toast.makeText(binding.root.context, "Connection success", Toast.LENGTH_SHORT).show(); } } override fun onConnectionFailedRtmp(reason: String) { runOnUiThread { Toast.makeText(binding.root.context, "Connection failed", Toast.LENGTH_SHORT).show(); rtmpCamera2.stopStream() setResult(NETWORK_ERROR) finish() } } override fun onNewBitrateRtmp(bitrate: Long) { } override fun onDisconnectRtmp() { runOnUiThread { Toast.makeText(binding.root.context, "Disconnect", Toast.LENGTH_SHORT).show(); } } override fun onAuthErrorRtmp() { runOnUiThread { Toast.makeText(binding.root.context, "Auth Error", Toast.LENGTH_SHORT).show(); } } override fun onAuthSuccessRtmp() { runOnUiThread { Toast.makeText(binding.root.context, "Auth Success", Toast.LENGTH_SHORT).show(); } } } rtmpCamera2 = RtmpCamera2(binding.surfaceView, connectChecker) var width = 1920 var height = 1080 when (streamData.resolution) { StreamData.STREAM_RESOLUTION.p2160 -> { width = 3840 height = 2160 } StreamData.STREAM_RESOLUTION.p1440 -> { width = 2560 height = 1440 } StreamData.STREAM_RESOLUTION.p1080 -> { width = 1920 height = 1080 } StreamData.STREAM_RESOLUTION.p720 -> { width = 1280 height = 720 } StreamData.STREAM_RESOLUTION.p480 -> { width = 640 height = 480 } StreamData.STREAM_RESOLUTION.p360 -> { width = 480 height = 360 } } rtmpCamera2.startPreview(CameraHelper.Facing.BACK, width,height) //start stream if (rtmpCamera2.prepareAudio() && rtmpCamera2.prepareVideo(width,height,30, (width*height*30*0.076).toInt(),CameraHelper.getCameraOrientation(this),true)) { println("peertubeurl "+streamData.url+"/"+streamData.key) rtmpCamera2.startStream(streamData.url+"/"+streamData.key) } else { /**This device cant init encoders, this could be for 2 reasons: The encoder selected doesnt support any configuration setted or your device hasnt a H264 or AAC encoder (in this case you can see log error valid encoder not found) */ } } private fun getUnAllowedPermissions(): List { val permissions: ArrayList = ArrayList() if (ContextCompat.checkSelfPermission(this, permission.CAMERA) != PackageManager.PERMISSION_GRANTED) { permissions.add(permission.CAMERA) } if (ContextCompat.checkSelfPermission(this, permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) { permissions.add(permission.RECORD_AUDIO) } return permissions } private fun askStopLive(reason: Int) { val alertBuilder = AlertDialog.Builder(this) alertBuilder.setTitle(R.string.end_stream) alertBuilder.setMessage(R.string.ask_end_stream) alertBuilder.setPositiveButton(R.string.yes) { _: DialogInterface, _: Int -> setResult(reason) finish() } alertBuilder.setNegativeButton(R.string.no,null) alertBuilder.show() } }