@@ -5,8 +5,14 @@ import android.content.ContentValues
55import android.content.Intent
66import android.content.pm.PackageManager
77import android.graphics.Bitmap
8+ import android.graphics.BitmapFactory
9+ import android.graphics.Canvas
10+ import android.media.AudioAttributes
11+ import android.media.AudioManager
812import android.media.MediaMetadataRetriever
13+ import android.media.MediaPlayer
914import android.net.Uri
15+ import android.os.Build
1016import android.os.Bundle
1117import android.os.Environment
1218import android.provider.MediaStore
@@ -15,6 +21,7 @@ import android.view.Menu
1521import android.view.MenuItem
1622import android.widget.SeekBar
1723import android.widget.Toast
24+ import androidx.annotation.RequiresApi
1825import androidx.appcompat.app.AlertDialog
1926import androidx.appcompat.app.AppCompatActivity
2027import androidx.core.app.ActivityCompat
@@ -40,11 +47,11 @@ class MainActivity : AppCompatActivity() {
4047 private val binding: ActivityMainBinding by lazy { ActivityMainBinding .inflate(layoutInflater) }
4148 private var videoUri: Uri ? = null
4249 private var videoName = " "
43- private var nbOfFrames = 0
44- private val mediaMetadataRetriever = MediaMetadataRetriever ()
45- private var frameIndex = 0
46- private var frameBitamp: Bitmap ? = null
50+ private var videoDuration = 0
51+ private var videoPosition = 0
4752 private var saveFrameMenuItem: MenuItem ? = null
53+ private var mediaPlayer: MediaPlayer ? = null
54+ private var mediaMetadataRetriever = MediaMetadataRetriever ()
4855
4956 override fun onCreate (savedInstanceState : Bundle ? ) {
5057 super .onCreate(savedInstanceState)
@@ -101,10 +108,13 @@ class MainActivity : AppCompatActivity() {
101108 else fatalError(" You must allow permissions !" )
102109 }
103110
111+ @RequiresApi(Build .VERSION_CODES .O )
104112 private fun onPermissionsAllowed () {
113+ BusyDialog .create(this )
114+
105115 binding.seekBarPosition.setOnSeekBarChangeListener(object : SeekBar .OnSeekBarChangeListener {
106116 override fun onProgressChanged (seekBar : SeekBar ? , progress : Int , p2 : Boolean ) {
107- setFrameIndex (progress)
117+ setVideoPosition (progress)
108118 }
109119
110120 override fun onStartTrackingTouch (p0 : SeekBar ? ) {
@@ -115,20 +125,30 @@ class MainActivity : AppCompatActivity() {
115125
116126 })
117127
118- binding.buttonSubMax.setOnClickListener { shiftFrameIndex(- 30 ) }
119- binding.buttonSubMed.setOnClickListener { shiftFrameIndex(- 5 ) }
120- binding.buttonSubMin.setOnClickListener { shiftFrameIndex(- 1 ) }
121- binding.buttonAddMin.setOnClickListener { shiftFrameIndex(1 ) }
122- binding.buttonAddMed.setOnClickListener { shiftFrameIndex(5 ) }
123- binding.buttonAddMax.setOnClickListener { shiftFrameIndex(30 ) }
128+ binding.buttonSubMax.setOnClickListener { shiftVideoPosition(- 2000 ) }
129+ binding.buttonSubMed.setOnClickListener { shiftVideoPosition(- 500 ) }
130+ binding.buttonSubMin.setOnClickListener { shiftVideoPosition(- 33 ) }
131+ binding.buttonAddMin.setOnClickListener { shiftVideoPosition(33 ) }
132+ binding.buttonAddMed.setOnClickListener { shiftVideoPosition(500 ) }
133+ binding.buttonAddMax.setOnClickListener { shiftVideoPosition(2000 ) }
134+
135+ binding.videoView.setAudioFocusRequest(AudioManager .AUDIOFOCUS_NONE )
136+ binding.videoView.setOnPreparedListener { newMediaPlayer ->
137+ newMediaPlayer.setVolume(0.0f , 0.0f )
138+ videoDuration = newMediaPlayer.duration
139+ saveFrameMenuItem?.isEnabled = true
140+ mediaPlayer = newMediaPlayer
141+ updateAll()
142+ Log .i(" [Video]" , " duration: $videoDuration " )
143+ }
124144
125145 setContentView(binding.root)
126146 }
127147
128148 override fun onCreateOptionsMenu (menu : Menu ? ): Boolean {
129149 menuInflater.inflate(R .menu.app_menu, menu)
130150 saveFrameMenuItem = menu?.findItem(R .id.menuSaveFrame)
131- saveFrameMenuItem?.isEnabled = null != frameBitamp
151+ saveFrameMenuItem?.isEnabled = null != videoUri && videoDuration > 0
132152 return true
133153 }
134154
@@ -145,39 +165,49 @@ class MainActivity : AppCompatActivity() {
145165 if (resultCode == RESULT_OK ) {
146166 if (requestCode == INTENT_OPEN_VIDEO ) {
147167 intent?.data?.let { uri -> openVideo(uri) }
148- return ;
168+ return
149169 }
150170 }
151171
152172 @Suppress(" DEPRECATION" )
153173 super .onActivityResult(requestCode, resultCode, intent)
154174 }
155175
156- private fun handleSaveFrame () {
157- val frameBitmap = this .frameBitamp ? : return
158- val fileName = " frame_${System .currentTimeMillis()} .png"
159- @Suppress(" DEPRECATION" )
160- val picturesDirectory = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_PICTURES )
161- val fileFullPath = " $picturesDirectory /$fileName "
176+ private fun saveFrame () {
162177 var success = false
178+ val fileName = " frame_${System .currentTimeMillis()} .png"
163179
164180 try {
165- val outputStream = File (fileFullPath).outputStream()
166- frameBitmap.compress(Bitmap .CompressFormat .PNG , 100 , outputStream)
167- outputStream.close()
168-
169- val values = ContentValues ()
170- @Suppress(" DEPRECATION" )
171- values.put(MediaStore .Images .Media .DATA , fileFullPath)
172- values.put(MediaStore .Images .Media .MIME_TYPE , " image/png" )
173- val newUri = contentResolver.insert(MediaStore .Images .Media .EXTERNAL_CONTENT_URI , values)
174- success = newUri != null
181+ mediaMetadataRetriever.setDataSource(applicationContext, videoUri)
182+ mediaMetadataRetriever.getFrameAtTime(videoPosition * 1000L , MediaMetadataRetriever .OPTION_CLOSEST )?.let { frameBitmap ->
183+ @Suppress(" DEPRECATION" )
184+ val picturesDirectory = Environment .getExternalStoragePublicDirectory(Environment .DIRECTORY_PICTURES )
185+ val fileFullPath = " $picturesDirectory /$fileName "
186+
187+ val outputStream = File (fileFullPath).outputStream()
188+ frameBitmap.compress(Bitmap .CompressFormat .PNG , 100 , outputStream)
189+ outputStream.close()
190+
191+ val values = ContentValues ()
192+ @Suppress(" DEPRECATION" )
193+ values.put(MediaStore .Images .Media .DATA , fileFullPath)
194+ values.put(MediaStore .Images .Media .MIME_TYPE , " image/png" )
195+ val newUri = contentResolver.insert(MediaStore .Images .Media .EXTERNAL_CONTENT_URI , values)
196+ success = newUri != null
197+ }
175198 } catch (e: Exception ) {
176199 e.printStackTrace()
177200 }
178201
202+ runOnUiThread {
203+ BusyDialog .dismiss()
204+ Toast .makeText(applicationContext, if (success) " Saved: $fileName " else " Failed !" , Toast .LENGTH_LONG ).show()
205+ }
206+ }
179207
180- Toast .makeText(applicationContext, if (success) " Saved: $fileName " else " Failed !" , Toast .LENGTH_LONG ).show()
208+ private fun handleSaveFrame () {
209+ BusyDialog .show(supportFragmentManager)
210+ GlobalScope .launch(Dispatchers .IO ) { saveFrame() }
181211 }
182212
183213 private fun handleOpenVideo () {
@@ -193,17 +223,16 @@ class MainActivity : AppCompatActivity() {
193223 }
194224
195225 private fun openVideo (videoUri : Uri ) {
226+ mediaPlayer = null
196227 this .videoUri = videoUri
197- mediaMetadataRetriever.setDataSource(applicationContext, videoUri)
198- nbOfFrames = mediaMetadataRetriever.extractMetadata( MediaMetadataRetriever . METADATA_KEY_VIDEO_FRAME_COUNT )?.toInt() ? : 0
228+ binding.videoView.setVideoURI( videoUri)
229+ videoDuration = 0
199230 videoName = DocumentFile .fromSingleUri(applicationContext, videoUri)?.name ? : " "
200-
201- Log .i(" [VideoFrame]" , " nbOfFrames: ${nbOfFrames} " )
202231 updateAll()
203232 }
204233
205234 private fun updateAll () {
206- val enabled = nbOfFrames > 0
235+ val enabled = videoDuration > 0
207236
208237 binding.seekBarPosition.isEnabled = enabled
209238 binding.seekBarPosition.progress = 0
@@ -213,50 +242,25 @@ class MainActivity : AppCompatActivity() {
213242 binding.buttonSubMin.isEnabled = enabled
214243 binding.buttonSubMed.isEnabled = enabled
215244 binding.buttonSubMax.isEnabled = enabled
216- saveFrameMenuItem?.isEnabled = false
245+ saveFrameMenuItem?.isEnabled = enabled
217246 binding.txtVideoName.text = if (enabled) videoName else " "
218- binding.txtFrameIndex.text = " "
219247
220248 if (enabled) {
221- binding.seekBarPosition.max = nbOfFrames
222- setFrameIndex (0 , true )
249+ binding.seekBarPosition.max = videoDuration
250+ setVideoPosition (0 , true )
223251 }
224252 }
225253
226- private fun shiftFrameIndex (delta : Int ) {
227- setFrameIndex(frameIndex + delta)
254+ private fun shiftVideoPosition (delta : Int ) {
255+ setVideoPosition(videoPosition + delta)
228256 }
229257
230- private fun setFrameIndex (newFrameIndex : Int , force : Boolean = false) {
231- if (! force && newFrameIndex == this .frameIndex) return
232- if (newFrameIndex < 0 || newFrameIndex >= nbOfFrames) return
233-
234- binding.txtFrameIndex.text = newFrameIndex.toString()
235- this .frameIndex = newFrameIndex
236- val currentVideoUri = this .videoUri
237- binding.seekBarPosition.progress = frameIndex
238- saveFrameMenuItem?.isEnabled = false
258+ private fun setVideoPosition (newVideoPosition : Int , force : Boolean = false) {
259+ if (! force && newVideoPosition == this .videoPosition) return
260+ if (newVideoPosition < 0 || newVideoPosition >= videoDuration || videoDuration <= 0 ) return
239261
240- GlobalScope .launch(Dispatchers .IO ) {
241- var newFrameBitamp: Bitmap ? = null
242-
243- try {
244- newFrameBitamp = mediaMetadataRetriever.getFrameAtIndex(frameIndex)
245- } catch (e: Exception ) {
246- e.printStackTrace()
247- }
248-
249- runOnUiThread {
250- if (frameIndex == newFrameIndex && videoUri == currentVideoUri) {
251- frameBitamp = newFrameBitamp
252- if (null == newFrameBitamp) {
253- binding.imageView.setImageResource(android.R .drawable.ic_menu_gallery)
254- } else {
255- binding.imageView.setImageBitmap(newFrameBitamp)
256- saveFrameMenuItem?.isEnabled = true
257- }
258- }
259- }
260- }
262+ this .videoPosition = newVideoPosition
263+ binding.seekBarPosition.progress = newVideoPosition
264+ mediaPlayer?.seekTo(videoPosition.toLong(), MediaPlayer .SEEK_CLOSEST )
261265 }
262266}
0 commit comments