Skip to content

Commit

Permalink
Min Android SDK 24
Browse files Browse the repository at this point in the history
1. Min Android SDK 24.
2. API: Added onRtspFirstFrameRendered to RtspStatusListener.
3. Fade in/out when image appeared/dissappeared in demo project.
4. Implemented getSnapshot() in demo project.
  • Loading branch information
alexeyvasilyev committed Dec 24, 2021
1 parent 062b239 commit 04c8caa
Show file tree
Hide file tree
Showing 10 changed files with 163 additions and 55 deletions.
2 changes: 1 addition & 1 deletion app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ android {

defaultConfig {
applicationId "com.alexvas.rtsp.demo"
minSdkVersion 21
minSdkVersion 24
targetSdkVersion 31
versionCode 1
versionName "1.0"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
package com.alexvas.rtsp.demo.ui.live
package com.alexvas.rtsp.demo.live

import android.annotation.SuppressLint
import android.graphics.Bitmap
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.HandlerThread
import android.text.Editable
import android.text.TextWatcher
import android.util.Log
import android.view.*
import android.widget.Toast
import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider
import com.alexvas.rtsp.demo.databinding.FragmentLiveBinding
import com.alexvas.rtsp.widget.RtspSurfaceView
import java.util.concurrent.atomic.AtomicBoolean


@SuppressLint("LogNotTimber")
Expand All @@ -23,6 +28,12 @@ class LiveFragment : Fragment() {
override fun onRtspStatusConnecting() {
binding.tvStatus.text = "RTSP connecting"
binding.pbLoading.visibility = View.VISIBLE
binding.vShutter.visibility = View.VISIBLE
binding.etRtspRequest.isEnabled = false
binding.etRtspUsername.isEnabled = false
binding.etRtspPassword.isEnabled = false
binding.cbVideo.isEnabled = false
binding.cbAudio.isEnabled = false
}

override fun onRtspStatusConnected() {
Expand All @@ -35,17 +46,53 @@ class LiveFragment : Fragment() {
binding.tvStatus.text = "RTSP disconnected"
binding.bnStartStop.text = "Start RTSP"
binding.pbLoading.visibility = View.GONE
binding.vShutter.visibility = View.VISIBLE
binding.bnSnapshot.isEnabled = false
binding.cbVideo.isEnabled = true
binding.cbAudio.isEnabled = true
binding.etRtspRequest.isEnabled = true
binding.etRtspUsername.isEnabled = true
binding.etRtspPassword.isEnabled = true
}

override fun onRtspStatusFailedUnauthorized() {
binding.tvStatus.text = "RTSP username or password invalid"
binding.pbLoading.visibility = View.GONE
Toast.makeText(context, binding.tvStatus.text , Toast.LENGTH_LONG).show()
}

override fun onRtspStatusFailed(message: String?) {
binding.tvStatus.text = "Error: $message"
Toast.makeText(context, binding.tvStatus.text , Toast.LENGTH_LONG).show()
binding.pbLoading.visibility = View.GONE
}

override fun onRtspFirstFrameRendered() {
binding.vShutter.visibility = View.GONE
binding.bnSnapshot.isEnabled = true
}
}

private fun getSnapshot(): Bitmap? {
if (DEBUG) Log.v(TAG, "getSnapshot()")
val surfaceBitmap = Bitmap.createBitmap(1920, 1080, Bitmap.Config.ARGB_8888)
val lock = Object()
val success = AtomicBoolean(false)
val thread = HandlerThread("PixelCopyHelper")
thread.start()
val sHandler = Handler(thread.looper)
val listener = PixelCopy.OnPixelCopyFinishedListener { copyResult ->
success.set(copyResult == PixelCopy.SUCCESS)
synchronized (lock) {
lock.notify()
}
}
synchronized (lock) {
PixelCopy.request(binding.svVideo.holder.surface, surfaceBitmap, listener, sHandler)
lock.wait()
}
thread.quitSafely()
return if (success.get()) surfaceBitmap else null
}

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Expand Down Expand Up @@ -114,20 +161,29 @@ class LiveFragment : Fragment() {
binding.svVideo.start(binding.cbVideo.isChecked, binding.cbAudio.isChecked)
}
}

binding.bnSnapshot.setOnClickListener {
val bitmap = getSnapshot()
// TODO Save snapshot to DCIM folder
if (bitmap != null) {
Toast.makeText(requireContext(), "Snapshot succeeded", Toast.LENGTH_LONG).show()
} else {
Toast.makeText(requireContext(), "Snapshot failed", Toast.LENGTH_LONG).show()
}
}
return binding.root
}

override fun onStart() {
if (DEBUG) Log.v(TAG, "onStart()")
super.onStart()
liveViewModel.loadParams(context)
override fun onResume() {
if (DEBUG) Log.v(TAG, "onResume()")
super.onResume()
liveViewModel.loadParams(requireContext())
}

override fun onStop() {
if (DEBUG) Log.v(TAG, "onStop()")
super.onStop()
liveViewModel.saveParams(context)
// onRtspClientStopped()
override fun onPause() {
if (DEBUG) Log.v(TAG, "onPause()")
super.onPause()
liveViewModel.saveParams(requireContext())
}

companion object {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.alexvas.rtsp.demo.ui.live
package com.alexvas.rtsp.demo.live

import android.annotation.SuppressLint
import android.content.Context
Expand Down Expand Up @@ -44,34 +44,34 @@ class LiveViewModel : ViewModel() {
// rtspRequest.value = "rtsp://10.0.1.3:554/axis-media/media.amp"
// }

fun loadParams(context: Context?) {
fun loadParams(context: Context) {
if (DEBUG)
Log.v(TAG, "loadParams()")
val pref = context?.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE)
val pref = context.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE)
try {
rtspRequest.setValue(pref?.getString(RTSP_REQUEST_KEY, DEFAULT_RTSP_REQUEST))
rtspRequest.setValue(pref.getString(RTSP_REQUEST_KEY, DEFAULT_RTSP_REQUEST))
} catch (e: ClassCastException) {
e.printStackTrace()
}
try {
rtspUsername.setValue(pref?.getString(RTSP_USERNAME_KEY, DEFAULT_RTSP_USERNAME))
rtspUsername.setValue(pref.getString(RTSP_USERNAME_KEY, DEFAULT_RTSP_USERNAME))
} catch (e: ClassCastException) {
e.printStackTrace()
}
try {
rtspPassword.setValue(pref?.getString(RTSP_PASSWORD_KEY, DEFAULT_RTSP_PASSWORD))
rtspPassword.setValue(pref.getString(RTSP_PASSWORD_KEY, DEFAULT_RTSP_PASSWORD))
} catch (e: ClassCastException) {
e.printStackTrace()
}
}

fun saveParams(context: Context?) {
fun saveParams(context: Context) {
if (DEBUG) Log.v(TAG, "saveParams()")
val editor = context?.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE)?.edit()
editor?.putString(RTSP_REQUEST_KEY, rtspRequest.value)
editor?.putString(RTSP_USERNAME_KEY, rtspUsername.value)
editor?.putString(RTSP_PASSWORD_KEY, rtspPassword.value)
editor?.apply()
val editor = context?.getSharedPreferences(LIVE_PARAMS_FILENAME, Context.MODE_PRIVATE).edit()
editor.putString(RTSP_REQUEST_KEY, rtspRequest.value)
editor.putString(RTSP_USERNAME_KEY, rtspUsername.value)
editor.putString(RTSP_PASSWORD_KEY, rtspPassword.value)
editor.apply()
}

}
7 changes: 7 additions & 0 deletions app/src/main/res/drawable/ic_camera_black_24dp.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path android:fillColor="#000" android:pathData="M4,4H7L9,2H15L17,4H20A2,2 0 0,1 22,6V18A2,2 0 0,1 20,20H4A2,2 0 0,1 2,18V6A2,2 0 0,1 4,4M12,7A5,5 0 0,0 7,12A5,5 0 0,0 12,17A5,5 0 0,0 17,12A5,5 0 0,0 12,7M12,9A3,3 0 0,1 15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9Z" />
</vector>
67 changes: 46 additions & 21 deletions app/src/main/res/layout/fragment_live.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ui.live.LiveFragment">
tools:context=".live.LiveFragment">

<LinearLayout
android:layout_width="match_parent"
Expand Down Expand Up @@ -50,52 +50,56 @@
</com.google.android.material.textfield.TextInputLayout>

<LinearLayout
android:layout_width="wrap_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="20dp"
android:orientation="vertical">
android:layout_marginTop="10dp"
android:gravity="center"
android:orientation="horizontal">
<CheckBox
android:id="@+id/cbVideo"
android:text="Video"
android:checked="true"
android:layout_width="match_parent"
android:layout_margin="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbAudio"
android:text="Audio"
android:layout_margin="10dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
<CheckBox
android:id="@+id/cbDebug"
android:text="Debug"
android:checked="true"
android:layout_marginTop="20dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

<TextView
android:id="@+id/tvStatus"
android:layout_marginTop="10dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
<!-- <CheckBox-->
<!-- android:id="@+id/cbDebug"-->
<!-- android:text="Debug"-->
<!-- android:checked="true"-->
<!-- android:layout_margin="20dp"-->
<!-- android:layout_width="wrap_content"-->
<!-- android:layout_height="wrap_content" />-->
</LinearLayout>

<Button
android:layout_marginTop="30dp"
android:layout_marginTop="10dp"
android:id="@+id/bnStartStop"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_margin="20dp"
android:layout_marginBottom="10dp"
android:text="Start RTSP" />

<FrameLayout
android:layout_width="match_parent"
android:layout_height="300dp">
android:layout_height="300dp"
android:animateLayoutChanges="true">
<com.alexvas.rtsp.widget.RtspSurfaceView
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/svVideo" />
<View
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:id="@+id/vShutter" />
<ProgressBar
android:layout_width="wrap_content"
android:layout_height="wrap_content"
Expand All @@ -104,6 +108,27 @@
android:id="@+id/pbLoading"/>
</FrameLayout>

<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="5dp"
android:orientation="horizontal"
android:gravity="center_vertical">
<Button
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/Widget.Material3.Button.TextButton.Icon"
android:id="@+id/bnSnapshot"
android:enabled="false"
android:text="Photo"
app:icon="@drawable/ic_camera_black_24dp"/>
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/tvStatus"
android:gravity="end"/>
</LinearLayout>

</LinearLayout>

</ScrollView>
2 changes: 1 addition & 1 deletion app/src/main/res/navigation/mobile_navigation.xml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@

<fragment
android:id="@+id/navigation_live"
android:name="com.alexvas.rtsp.demo.ui.live.LiveFragment"
android:name="com.alexvas.rtsp.demo.live.LiveFragment"
android:label="@string/title_live"
tools:layout="@layout/fragment_live" />

Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ buildscript {

ext.kotlin_version = '1.6.10'
ext.compile_sdk_version = 31
ext.min_sdk_version = 21
ext.min_sdk_version = 24
ext.target_sdk_version = 31
ext.project_version_code = 140
ext.project_version_name = '1.4.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ class AudioDecodeThread (

companion object {
private val TAG: String = AudioDecodeThread::class.java.simpleName
private const val DEBUG = true
private const val DEBUG = false

fun getAacDecoderConfigData(audioProfile: Int, sampleRate: Int, channels: Int): ByteArray {
// AOT_LC = 2
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.alexvas.rtsp.codec

import android.media.MediaCodec
import android.media.MediaCodec.OnFrameRenderedListener
import android.media.MediaFormat
import android.util.Log
import android.view.Surface
Expand All @@ -11,7 +12,8 @@ class VideoDecodeThread (
private val mimeType: String,
private val width: Int,
private val height: Int,
private val videoFrameQueue: FrameQueue) : Thread() {
private val videoFrameQueue: FrameQueue,
private val onFrameRenderedListener: OnFrameRenderedListener) : Thread() {

private var isRunning = true

Expand Down Expand Up @@ -41,6 +43,7 @@ class VideoDecodeThread (
// try {
if (DEBUG) Log.d(TAG, "Configuring surface ${safeWidth}x${safeHeight} w/ '$mimeType'")
decoder.configure(format, surface, null, 0)
decoder.setOnFrameRenderedListener(onFrameRenderedListener, null)
decoder.start()
if (DEBUG) Log.d(TAG, "Started surface")
// } catch (e: IllegalArgumentException) {
Expand Down Expand Up @@ -105,7 +108,7 @@ class VideoDecodeThread (

companion object {
private val TAG: String = VideoDecodeThread::class.java.simpleName
private const val DEBUG = true
private const val DEBUG = false
}

}
Expand Down
Loading

0 comments on commit 04c8caa

Please sign in to comment.