diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 13931c7..f8c8ba2 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ tools:targetApi="31"> + private lateinit var progressView: ProgressView + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() @@ -81,14 +107,17 @@ class KaraokePlayerActivity : WokaBaseActivity() { WindowCompat.getInsetsController(window, window.decorView) windowInsetsController.hide(WindowInsetsCompat.Type.systemBars()) + progressView = ProgressView(this, getString(R.string.please_wait)) + progressView.show() + karaokePlayerData = intent.getParcelableExtra(EXTRA_KARAOKE_DATA) - networkCallback = object : ConnectivityManager.NetworkCallback(){ + networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { super.onAvailable(network) runOnUiThread { - if (playbackState == PlayBackState.STOPPED){ - binding.playerView.show() + if (playbackState == PlayBackState.STOPPED) { + binding.mainView.show() binding.errorView.hide() playVideo() } @@ -100,11 +129,13 @@ class KaraokePlayerActivity : WokaBaseActivity() { binding.playerView.player = player recordingState = RecordingState.NOT_RECORDING - recordingOutputPath = "${externalCacheDir?.absolutePath}/karaoke.3gp" + recordingOutputPath = "${externalCacheDir?.absolutePath}/karaoke_recording.3gp" + karaokeMusicPath = "${externalCacheDir?.absolutePath}/karaoke_music" + karaokeFinalPath = "${externalCacheDir?.absolutePath}/karaoke_final_output" addListeners() - playVideo() + updateRecordingUI() initViews() @@ -112,18 +143,20 @@ class KaraokePlayerActivity : WokaBaseActivity() { registerLaunchers() + loadAudioFromUrl() + } override fun onStart() { super.onStart() - if (player?.isPlaying == false){ + if (player?.isPlaying == false) { player?.play() } } override fun onStop() { super.onStop() - if (player?.isPlaying == true){ + if (player?.isPlaying == true) { player?.pause() } } @@ -145,20 +178,20 @@ class KaraokePlayerActivity : WokaBaseActivity() { ) } - private fun initViews(){ + private fun initViews() { binding.apply { - playerView.findViewById(R.id.player_controller_title).text = karaokePlayerData?.title + title.text = karaokePlayerData?.title } } - private fun clickEvents(){ + private fun clickEvents() { binding.apply { - playerView.findViewById(R.id.player_controller_close_btn).setOnClickListener { + backBtn.setOnClickListener { onBackPressedDispatcher.onBackPressed() } retryBtn.setOnClickListener { - binding.playerView.show() + binding.mainView.show() binding.errorView.hide() playVideo() } @@ -177,76 +210,106 @@ class KaraokePlayerActivity : WokaBaseActivity() { } } - private fun registerLaunchers(){ + private fun registerLaunchers() { permissionLauncher = registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted -> if (isGranted) { startStopRecording() - }else{ + } else { toast(getString(R.string.permission_deniedd)) } } } - private fun playStopPlayingAudio(){ - when (recordingState) { - RecordingState.NOT_RECORDING -> { - if (audioPlayer != null){ - audioPlayer?.release() - audioPlayer = null + private fun updateRecordingUI() { + binding.apply { + when (recordingState) { + RecordingState.RECORDING -> { + recordBtn.text = getString(R.string.stop_recording) + recordBtn.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_rec_mic_cross, + 0, + 0, + 0 + ) + + playerView.useController = false + + audioBtn.isEnabled = false + audioBtn.text = getString(R.string.play) + audioBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_play, 0, 0, 0) + + downloadBtn.isEnabled = false } - audioPlayer = MediaPlayer().apply { - try { - setDataSource(this@KaraokePlayerActivity, Uri.parse(recordingOutputPath)) - prepare() - start() + RecordingState.NOT_RECORDING -> { + recordBtn.text = getString(R.string.stop_recording) + recordBtn.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_rec_mic, + 0, + 0, + 0 + ) - recordingState = RecordingState.PLAYING_AUDIO + playerView.useController = true - setOnCompletionListener { - audioPlayer?.release() - audioPlayer = null + audioBtn.isEnabled = true + audioBtn.text = getString(R.string.play) + audioBtn.setCompoundDrawablesWithIntrinsicBounds(R.drawable.ic_play, 0, 0, 0) - recordingState = RecordingState.NOT_RECORDING - } - }catch (e: Exception) { - Log.d(TAG, "setupAudioPlayer: $e") - toast(getString(R.string.something_went_wrong)) + downloadBtn.isEnabled = true + } + + RecordingState.PLAYING_AUDIO -> { + recordBtn.text = getString(R.string.stop_recording) + recordBtn.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_rec_mic, + 0, + 0, + 0 + ) + + playerView.useController = true + + audioBtn.isEnabled = true + if (audioPlayer?.isPlaying == true) { + audioBtn.text = getString(R.string.pause) + audioBtn.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_pause, + 0, + 0, + 0 + ) + } else { + audioBtn.text = getString(R.string.play) + audioBtn.setCompoundDrawablesWithIntrinsicBounds( + R.drawable.ic_play, + 0, + 0, + 0 + ) } - } - } - RecordingState.RECORDING -> { - toast(getString(R.string.recording_audio)) - } - RecordingState.PLAYING_AUDIO -> { - if (audioPlayer?.isPlaying == true){ - audioPlayer?.pause() - }else{ - audioPlayer?.start() + + downloadBtn.isEnabled = true } } } } - private fun playVideo() { - if (karaokePlayerData?.karaokeVideoUrl == null) return - - player?.setMediaItem(MediaItem.fromUri(karaokePlayerData?.karaokeVideoUrl!!)) - player?.prepare() - player?.play() - } - - private fun addListeners(){ - player?.addListener(object : Player.Listener{ + private fun addListeners() { + player?.addListener(object : Player.Listener { override fun onIsPlayingChanged(isPlaying: Boolean) { super.onIsPlayingChanged(isPlaying) - playbackState = if (isPlaying){ - binding.playerView.show() + playbackState = if (isPlaying) { + binding.mainView.show() binding.errorView.hide() + if (audioPlayer?.isPlaying == true) { + audioPlayer?.pause() + updateRecordingUI() + } PlayBackState.PLAY - }else{ + } else { PlayBackState.PAUSED } } @@ -254,7 +317,7 @@ class KaraokePlayerActivity : WokaBaseActivity() { override fun onPlayerError(error: PlaybackException) { super.onPlayerError(error) playbackState = PlayBackState.STOPPED - binding.playerView.hide() + binding.mainView.hide() binding.errorView.show() } }) @@ -266,6 +329,108 @@ class KaraokePlayerActivity : WokaBaseActivity() { ) } + private fun loadAudioFromUrl() { + if (karaokePlayerData?.karaokeVideoUrl == null) { + progressView.hide() + toast(getString(R.string.canoot_load_karaoke)) + playVideo() + return + } + + CoroutineScope(Dispatchers.IO).launch { + try { + val url = URL(karaokePlayerData?.karaokeVideoUrl) + copyStreamToFile(url.openConnection().getInputStream(), File(karaokeMusicPath)) + runOnUiThread { + binding.recorderView.show() + } + } catch (e: Exception) { + runOnUiThread { + toast(getString(R.string.canoot_load_karaoke)) + binding.recorderView.hide() + } + } finally { + runOnUiThread { + playVideo() + progressView.hide() + } + } + } + } + + private fun playStopPlayingAudio() { + when (recordingState) { + RecordingState.NOT_RECORDING -> { + if (audioPlayer != null) { + audioPlayer?.release() + audioPlayer = null + } + + audioPlayer = MediaPlayer().apply { + try { + setDataSource(this@KaraokePlayerActivity, Uri.parse(karaokeFinalPath)) + prepare() + + setOnCompletionListener { + audioPlayer?.release() + audioPlayer = null + + recordingState = RecordingState.NOT_RECORDING + updateRecordingUI() + } + } catch (e: Exception) { + toast(getString(R.string.something_went_wrong)) + } + } + + try { + if (player?.isPlaying == true) { + player?.pause() + } + audioPlayer?.start() + + recordingState = RecordingState.PLAYING_AUDIO + updateRecordingUI() + } catch (e: Exception) { + Log.d(TAG, "playStopPlayingAudio: $e") + toast(getString(R.string.something_went_wrong)) + recordingState = RecordingState.NOT_RECORDING + audioPlayer?.release() + audioPlayer = null + } + } + + RecordingState.RECORDING -> { + toast(getString(R.string.recording_audio)) + updateRecordingUI() + } + + RecordingState.PLAYING_AUDIO -> { + if (audioPlayer?.isPlaying == true) { + audioPlayer?.pause() + updateRecordingUI() + } else { + if (player?.isPlaying == true) { + player?.pause() + } + audioPlayer?.start() + updateRecordingUI() + } + } + } + } + + private fun playVideo() { + if (karaokePlayerData?.karaokeVideoUrl == null) return + binding.playerView.show() + player?.setMediaItem(MediaItem.fromUri(karaokePlayerData?.karaokeVideoUrl!!)) + + player?.prepare() + player?.play() + } + + + @OptIn(UnstableApi::class) private fun startStopRecording() { if (ActivityCompat.checkSelfPermission( this, @@ -276,34 +441,131 @@ class KaraokePlayerActivity : WokaBaseActivity() { return } - if (recordingState == RecordingState.NOT_RECORDING){ - recorder = MediaRecorder().apply { - setAudioSource(MediaRecorder.AudioSource.MIC) - setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) - setOutputFile(recordingOutputPath) - setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + when (recordingState) { + RecordingState.NOT_RECORDING -> { + startRecording() + } + RecordingState.RECORDING -> { try { - prepare() - start() - toast(getString(R.string.recording_started)) - recordingState = RecordingState.RECORDING + stopRecording() } catch (e: Exception) { - toast(getString(R.string.something_went_wrong)) + // do nothing + } finally { + if (player?.isPlaying == true) player?.pause() + updateRecordingUI() + binding.playerView.showController() } } - }else if (recordingState == RecordingState.RECORDING){ - stopRecording() + + RecordingState.PLAYING_AUDIO -> { + startRecording() + } + } + } + + private fun startRecording() { + recorder = MediaRecorder().apply { + setAudioSource(MediaRecorder.AudioSource.MIC) + setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP) + setOutputFile(recordingOutputPath) + setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB) + + try { + prepare() + if (audioPlayer?.isPlaying == true) { + audioPlayer?.pause() + } + + player?.play() + startRecordingPos = player?.currentPosition + start() + + toast(getString(R.string.recording_started)) + recordingState = RecordingState.RECORDING + updateRecordingUI() + } catch (e: Exception) { + toast(getString(R.string.something_went_wrong)) + } } } private fun stopRecording() { recorder?.apply { stop() - release() recordingState = RecordingState.NOT_RECORDING toast(getString(R.string.recording_stopped)) + + release() + + mixAudios() + } recorder = null } + + private fun mixAudios() { + try { + if (startRecordingPos != null){ + val input1 = GeneralAudioInput(karaokeMusicPath) + input1.startTimeUs = startRecordingPos!! * 1000 + input1.startOffsetUs = 100_000 + getAudioDuration(recordingOutputPath)?.let { + input1.endTimeUs = input1.startTimeUs + (it * 1000) + } + val input2 = GeneralAudioInput(recordingOutputPath) + + val audioMixer = AudioMixer(karaokeFinalPath) + audioMixer.addDataSource(input1) + audioMixer.addDataSource(input2) + audioMixer.isLoopingEnabled = false + audioMixer.mixingType = AudioMixer.MixingType.PARALLEL + audioMixer.setProcessingListener(object : AudioMixer.ProcessingListener{ + override fun onProgress(progress: Double) { + runOnUiThread { + Log.d("aditya_test", "onProgress: ${(progress * 100).toInt()}") + } + } + + override fun onEnd() { + runOnUiThread { + Log.d(TAG, "onEnd: ") + audioMixer.release() + } + } + }) + + audioMixer.start() + + audioMixer.processAsync() + }else{ + throw Exception() + } + } catch (e: Exception) { + toast(getString(R.string.karaoke_process_failed)) + } + } + + private fun getAudioDuration(audio: String):Long? { + val mmr = MediaMetadataRetriever() + mmr.setDataSource(this, Uri.parse(audio)) + val durationStr = mmr.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION) + return durationStr?.toLongOrNull() + } + + private fun copyStreamToFile(inputStream: InputStream, outputFile: File) { + inputStream.use { input -> + val outputStream = FileOutputStream(outputFile) + outputStream.use { output -> + val buffer = ByteArray(4 * 1024) // buffer size + while (true) { + val byteCount = input.read(buffer) + if (byteCount < 0) break + output.write(buffer, 0, byteCount) + } + output.flush() + } + } + } + } \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_mic.xml b/app/src/main/res/drawable/ic_mic.xml index bd90ce0..6de6b1f 100644 --- a/app/src/main/res/drawable/ic_mic.xml +++ b/app/src/main/res/drawable/ic_mic.xml @@ -1,5 +1,5 @@ - + - + diff --git a/app/src/main/res/drawable/ic_pause.xml b/app/src/main/res/drawable/ic_pause.xml index dfc18af..a70358f 100644 --- a/app/src/main/res/drawable/ic_pause.xml +++ b/app/src/main/res/drawable/ic_pause.xml @@ -1,7 +1,14 @@ - + - - + + diff --git a/app/src/main/res/drawable/ic_play.xml b/app/src/main/res/drawable/ic_play.xml index d7fc626..74cc287 100644 --- a/app/src/main/res/drawable/ic_play.xml +++ b/app/src/main/res/drawable/ic_play.xml @@ -1,7 +1,14 @@ - + - - + + diff --git a/app/src/main/res/drawable/ic_rec_mic.xml b/app/src/main/res/drawable/ic_rec_mic.xml new file mode 100644 index 0000000..870a419 --- /dev/null +++ b/app/src/main/res/drawable/ic_rec_mic.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_rec_mic_cross.xml b/app/src/main/res/drawable/ic_rec_mic_cross.xml new file mode 100644 index 0000000..758766e --- /dev/null +++ b/app/src/main/res/drawable/ic_rec_mic_cross.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/layout/activity_karaoke_playerr.xml b/app/src/main/res/layout/activity_karaoke_playerr.xml index fa55911..f704bf1 100644 --- a/app/src/main/res/layout/activity_karaoke_playerr.xml +++ b/app/src/main/res/layout/activity_karaoke_playerr.xml @@ -12,24 +12,79 @@ android:id="@+id/main_view" android:layout_width="match_parent" android:layout_height="match_parent" - > + + + + + + + + + + android:layout_below="@id/player_view" + android:layout_marginTop="60dp" + > + android:layout_marginBottom="32dp"/> - - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 34002be..7141644 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -204,10 +204,13 @@ SING NOW Start Recording Download Recording - Play Recording Permission Denied. Recording Started Recording Stopped. Recording No recording found + Stop Recording + PAUSE + Cannot load karaoke + Karaoke process failed. \ No newline at end of file