Skip to content

Commit 46c7e4d

Browse files
author
Dan Oprea
committed
Use VideoView / MediaPlayer to find the frame much faster (unfortunately I still need MediaMetadataRetriever to extract the frame).
Upgrade min SDK to 26
1 parent aa034ab commit 46c7e4d

File tree

3 files changed

+85
-90
lines changed

3 files changed

+85
-90
lines changed

app/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ android {
2121

2222
defaultConfig {
2323
applicationId "com.dan.videoframe"
24-
minSdkVersion 24
24+
minSdkVersion 26
2525
targetSdkVersion 30
2626
versionCode 1
2727
versionName "1.0"

app/src/main/java/com/dan/videoframe/MainActivity.kt

Lines changed: 75 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,14 @@ import android.content.ContentValues
55
import android.content.Intent
66
import android.content.pm.PackageManager
77
import android.graphics.Bitmap
8+
import android.graphics.BitmapFactory
9+
import android.graphics.Canvas
10+
import android.media.AudioAttributes
11+
import android.media.AudioManager
812
import android.media.MediaMetadataRetriever
13+
import android.media.MediaPlayer
914
import android.net.Uri
15+
import android.os.Build
1016
import android.os.Bundle
1117
import android.os.Environment
1218
import android.provider.MediaStore
@@ -15,6 +21,7 @@ import android.view.Menu
1521
import android.view.MenuItem
1622
import android.widget.SeekBar
1723
import android.widget.Toast
24+
import androidx.annotation.RequiresApi
1825
import androidx.appcompat.app.AlertDialog
1926
import androidx.appcompat.app.AppCompatActivity
2027
import 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
}

app/src/main/res/layout/activity_main.xml

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,6 @@
1717
android:textAppearance="@style/TextAppearance.AppCompat.Large"
1818
android:textStyle="bold" />
1919

20-
<TextView
21-
android:id="@+id/txtFrameIndex"
22-
android:layout_width="match_parent"
23-
android:layout_height="wrap_content"
24-
android:textAlignment="center"
25-
android:textStyle="bold" />
26-
2720
<LinearLayout
2821
android:layout_width="match_parent"
2922
android:layout_height="wrap_content"
@@ -36,7 +29,7 @@
3629
android:layout_width="wrap_content"
3730
android:layout_height="wrap_content"
3831
android:layout_weight="1"
39-
android:text="-30"
32+
android:text="&lt;&lt;&lt;"
4033
android:textColor="@color/purple_700" />
4134

4235
<Button
@@ -45,7 +38,7 @@
4538
android:layout_width="wrap_content"
4639
android:layout_height="wrap_content"
4740
android:layout_weight="1"
48-
android:text="-5"
41+
android:text="&lt;&lt;"
4942
android:textColor="@color/purple_700" />
5043

5144
<Button
@@ -54,7 +47,7 @@
5447
android:layout_width="wrap_content"
5548
android:layout_height="wrap_content"
5649
android:layout_weight="1"
57-
android:text="-1"
50+
android:text="&lt;"
5851
android:textColor="@color/purple_700" />
5952

6053
<Button
@@ -63,7 +56,7 @@
6356
android:layout_width="wrap_content"
6457
android:layout_height="wrap_content"
6558
android:layout_weight="1"
66-
android:text="+1"
59+
android:text="&gt;"
6760
android:textColor="@color/purple_700" />
6861

6962
<Button
@@ -72,7 +65,7 @@
7265
android:layout_width="wrap_content"
7366
android:layout_height="wrap_content"
7467
android:layout_weight="1"
75-
android:text="+5"
68+
android:text="&gt;&gt;"
7669
android:textColor="@color/purple_700" />
7770

7871
<Button
@@ -81,7 +74,7 @@
8174
android:layout_width="wrap_content"
8275
android:layout_height="wrap_content"
8376
android:layout_weight="1"
84-
android:text="+30"
77+
android:text="&gt;&gt;&gt;"
8578
android:textColor="@color/purple_700" />
8679
</LinearLayout>
8780

@@ -91,15 +84,13 @@
9184
android:layout_height="wrap_content"
9285
android:padding="16dip" />
9386

94-
<ImageView
95-
android:id="@+id/imageView"
87+
<VideoView
88+
android:id="@+id/videoView"
9689
android:layout_width="match_parent"
9790
android:layout_height="match_parent"
9891
android:layout_margin="16dp"
9992
android:adjustViewBounds="false"
100-
android:baselineAlignBottom="false"
101-
android:scaleType="fitCenter"
102-
app:srcCompat="@android:drawable/ic_menu_gallery" />
93+
android:scaleType="fitCenter" />
10394
</LinearLayout>
10495

10596
</layout>

0 commit comments

Comments
 (0)