Skip to content

Commit

Permalink
Saving to gallery.
Browse files Browse the repository at this point in the history
* Added delete button to player.
* Updated readme.
* Bump version number.

Fixes #5
  • Loading branch information
zegkljan committed Mar 15, 2022
1 parent 705142e commit f23b0f3
Show file tree
Hide file tree
Showing 10 changed files with 106 additions and 61 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,15 @@
An extremely simple app whose only purpose is to take a (high speed) video and then lets the user to analyze it - slow it down, step one frame at a time.
The use case is mainly as a poor-man's eagle eye, i.e. for video review of sport situations, especially HEMA (Historical European Martial Arts) fencing actions.

The app does not save the video (or, more precisely, once the user is done with the analysis and returns to the viewfinder, the video file is deleted).

## Workflow
The app has the following workflow:

0. Lanuch the app.
0. Launch the app.
1. Ask for permissions (if not already granted).
2. Select capture resolution and frame rate.
3. Capture video.
4. Seek through, slow down...
5. Tap the ✓ button to go back to step 3, or press the native back button to go to step 2.
5. Tap the ✓ button to go back to step 3 (or the bin button to delete the video and then go to step 3), or press the native back button to go to step 2.

## Download and installation
Scan this QR code to download the APK:
Expand Down
4 changes: 2 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ android {
compileSdkVersion 31
defaultConfig {
applicationId "cz.zegkljan.videoreferee"
versionCode 3
versionName "0.3.0"
versionCode 4
versionName "0.4.0"
minSdkVersion 24
targetSdkVersion 31
testInstrumentationRunner "android.test.InstrumentationTestRunner"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,9 @@ import androidx.navigation.Navigation
import androidx.navigation.fragment.navArgs
import cz.zegkljan.videoreferee.R
import cz.zegkljan.videoreferee.databinding.FragmentCameraBinding
import cz.zegkljan.videoreferee.utils.MediaItem
import cz.zegkljan.videoreferee.utils.Medium
import cz.zegkljan.videoreferee.utils.OrientationLiveData
import cz.zegkljan.videoreferee.utils.createDummyFile
import cz.zegkljan.videoreferee.utils.prepareMediaItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -112,7 +111,7 @@ class HighSpeedCameraFragment : Fragment() {
/** The [CameraDevice] that will be opened in this fragment */
private lateinit var camera: CameraDevice

private var mediaItem: MediaItem? = null
private var medium: Medium? = null

/** Requests used for preview only in the [CameraConstrainedHighSpeedCaptureSession] */
private val previewRequestList: List<CaptureRequest> by lazy {
Expand Down Expand Up @@ -256,8 +255,8 @@ class HighSpeedCameraFragment : Fragment() {

// Sets the output file
val ctx = requireContext()
mediaItem = prepareMediaItem(ctx, "mp4")
setOutputFile(mediaItem!!.getWriteFileDescriptor(ctx))
medium = Medium.create(ctx, "mp4")
setOutputFile(medium!!.getWriteFileDescriptor(ctx))

prepare()
start()
Expand All @@ -280,8 +279,8 @@ class HighSpeedCameraFragment : Fragment() {
recorder.stop()

// Finalize output file
mediaItem!!.closeFileDescriptor()
mediaItem!!.finalize(requireContext())
medium!!.closeFileDescriptor()
medium!!.finalize(requireContext())

// Unlocks screen rotation after recording finished
requireActivity().requestedOrientation =
Expand All @@ -291,7 +290,7 @@ class HighSpeedCameraFragment : Fragment() {
requireActivity().runOnUiThread {
navController.navigate(
HighSpeedCameraFragmentDirections.actionHighSpeedCameraToPlayer(
mediaItem!!.getUriString(),
medium!!.getUriString(),
args.cameraId,
args.width,
args.height,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ import androidx.navigation.Navigation
import androidx.navigation.fragment.navArgs
import cz.zegkljan.videoreferee.R
import cz.zegkljan.videoreferee.databinding.FragmentCameraBinding
import cz.zegkljan.videoreferee.utils.MediaItem
import cz.zegkljan.videoreferee.utils.Medium
import cz.zegkljan.videoreferee.utils.OrientationLiveData
import cz.zegkljan.videoreferee.utils.createDummyFile
import cz.zegkljan.videoreferee.utils.prepareMediaItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
Expand Down Expand Up @@ -111,7 +110,7 @@ class NormalSpeedCameraFragment : Fragment() {
/** The [CameraDevice] that will be opened in this fragment */
private lateinit var camera: CameraDevice

private var mediaItem: MediaItem? = null
private var medium: Medium? = null

/** Request used for preview only in the [CameraCaptureSession] */
private val previewRequest: CaptureRequest by lazy {
Expand Down Expand Up @@ -236,9 +235,9 @@ class NormalSpeedCameraFragment : Fragment() {
relativeOrientation.value?.let { setOrientationHint(it) }
// Sets the output file
val ctx = requireContext()
mediaItem = prepareMediaItem(ctx, "mp4")
Log.d(TAG, mediaItem.toString())
setOutputFile(mediaItem!!.getWriteFileDescriptor(ctx))
medium = Medium.create(ctx, "mp4")
Log.d(TAG, medium.toString())
setOutputFile(medium!!.getWriteFileDescriptor(ctx))

prepare()
start()
Expand All @@ -261,8 +260,8 @@ class NormalSpeedCameraFragment : Fragment() {
recorder.stop()

// Finalize output file
mediaItem!!.closeFileDescriptor()
mediaItem!!.finalize(requireContext())
medium!!.closeFileDescriptor()
medium!!.finalize(requireContext())

// Unlocks screen rotation after recording finished
requireActivity().requestedOrientation =
Expand All @@ -272,7 +271,7 @@ class NormalSpeedCameraFragment : Fragment() {
requireActivity().runOnUiThread {
navController.navigate(
NormalSpeedCameraFragmentDirections.actionNormalSpeedCameraToPlayer(
mediaItem!!.getUriString(),
medium!!.getUriString(),
args.cameraId,
args.width,
args.height,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import com.google.android.exoplayer2.Player
import com.google.android.exoplayer2.SimpleExoPlayer
import cz.zegkljan.videoreferee.R
import cz.zegkljan.videoreferee.databinding.FragmentPlayerBinding
import cz.zegkljan.videoreferee.utils.Medium
import kotlin.math.roundToInt

class PlayerFragment : Fragment() {
Expand Down Expand Up @@ -167,12 +168,18 @@ class PlayerFragment : Fragment() {

// navigation out
fragmentPlayerBinding.doneButton.setOnClickListener {
/*
val file = File(args.filename)
if (!file.delete()) {
// Log.e(TAG, "Failed to delete file $file")
val navDirections: NavDirections = if (args.isHighSpeed) {
PlayerFragmentDirections.actionPlayerToHighSpeedCamera(args.cameraId, args.width, args.height, args.fps)
} else {
PlayerFragmentDirections.actionPlayerToNormalSpeedCamera(args.cameraId, args.width, args.height, args.fps)
}
navController.navigate(navDirections)
}
fragmentPlayerBinding.deleteButton.setOnClickListener {
val medium = Medium.fromUri(Uri.parse(args.fileuri))
if (!medium.remove(requireContext())) {
// Log.e(TAG, "Failed to delete file $medium")
}
*/
val navDirections: NavDirections = if (args.isHighSpeed) {
PlayerFragmentDirections.actionPlayerToHighSpeedCamera(args.cameraId, args.width, args.height, args.fps)
} else {
Expand Down
88 changes: 55 additions & 33 deletions app/src/main/java/cz/zegkljan/videoreferee/utils/MediaUtils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
package cz.zegkljan.videoreferee.utils

import android.annotation.SuppressLint
import android.content.ContentUris
import android.content.ContentValues
import android.content.Context
import android.media.MediaScannerConnection
Expand All @@ -27,6 +28,7 @@ import android.os.Environment
import android.os.ParcelFileDescriptor
import android.provider.MediaStore
import android.util.Log
import androidx.core.net.toFile
import androidx.core.net.toUri
import java.io.File
import java.io.FileDescriptor
Expand All @@ -42,14 +44,49 @@ fun createDummyFile(context: Context): File {
return File(context.filesDir, "dummyfile")
}

abstract class MediaItem {
abstract class Medium {
abstract fun getUriString(): String
abstract fun getWriteFileDescriptor(context: Context): FileDescriptor
abstract fun closeFileDescriptor()
open fun finalize(context: Context) = Unit
abstract fun remove(context: Context): Boolean

companion object {
/** Creates a [Medium] named with the current date and time */
fun create(context: Context, extension: String): Medium {
// Log.d(TAG, "createFile")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val videoDetails = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, "VID_${SDF.format(Date())}.$extension")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/VideoReferee/")
put(MediaStore.Video.Media.IS_PENDING, 1)
}
val videoUri = resolver.insert(videoCollection, videoDetails)
Log.d(TAG, videoUri.toString())

return MediaStoreMedium(videoUri!!)
} else {
val externalFilesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
val videoRefereeDir = File(externalFilesDir, "VideoReferee")
videoRefereeDir.mkdirs()
val file = File(videoRefereeDir, "VID_${SDF.format(Date())}.$extension")
return FileMedium(file)
}
}

/** Creates a [Medium] from the given [Uri] */
fun fromUri(uri: Uri): Medium = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStoreMedium(uri)
} else {
FileMedium(uri.toFile())
}
}
}

private class MediaStoreItem(val uri: Uri) : MediaItem() {
private class MediaStoreMedium(val uri: Uri) : Medium() {
var fd: ParcelFileDescriptor? = null

override fun getUriString(): String {
Expand All @@ -76,12 +113,21 @@ private class MediaStoreItem(val uri: Uri) : MediaItem() {
}, null, null)
}

@SuppressLint("InlinedApi")
override fun remove(context: Context): Boolean {
val resolver = context.contentResolver

val videoCollection = MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)

return 0 < resolver.delete(videoCollection, "${MediaStore.Audio.Media._ID} = ?", arrayOf(ContentUris.parseId(uri).toString()))
}

override fun toString(): String {
return "MediaStoreItem($uri)"
}
}

private class FileItem(val file: File) : MediaItem() {
private class FileMedium(val file: File) : Medium() {
var fis: FileOutputStream? = null

override fun getUriString(): String {
Expand All @@ -99,41 +145,17 @@ private class FileItem(val file: File) : MediaItem() {
}
fis!!.close()
}

override fun toString(): String {
return "FileItem($file)"
override fun remove(context: Context): Boolean {
val deleted = file.delete()
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), arrayOfNulls(1), null)
return deleted
}

override fun finalize(context: Context) {
MediaScannerConnection.scanFile(context, arrayOf(file.absolutePath), arrayOfNulls(1), null)
}
}

/** Creates a media [Uri] named with the current date and time */
fun prepareMediaItem(context: Context, extension: String): MediaItem {
// Log.d(TAG, "createFile")

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val resolver = context.contentResolver
val videoCollection = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
} else {
MediaStore.Video.Media.EXTERNAL_CONTENT_URI
}
val videoDetails = ContentValues().apply {
put(MediaStore.Video.Media.DISPLAY_NAME, "VID_${SDF.format(Date())}.$extension")
put(MediaStore.MediaColumns.RELATIVE_PATH, "DCIM/VideoReferee/")
put(MediaStore.Video.Media.IS_PENDING, 1)
}
val videoUri = resolver.insert(videoCollection, videoDetails)
Log.d(TAG, videoUri.toString())

return MediaStoreItem(videoUri!!)
} else {
val externalFilesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)
val videoRefereeDir = File(externalFilesDir, "VideoReferee")
videoRefereeDir.mkdirs()
val file = File(videoRefereeDir, "VID_${SDF.format(Date())}.$extension")
return FileItem(file)
override fun toString(): String {
return "FileItem($file)"
}
}
5 changes: 5 additions & 0 deletions app/src/main/res/drawable/ic_baseline_delete_24.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
13 changes: 13 additions & 0 deletions app/src/main/res/layout/fragment_player.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,19 @@
app:layout_constraintRight_toRightOf="parent"
tools:ignore="RtlHardcoded" />

<Button
android:id="@+id/delete_button"
android:layout_width="32dp"
android:layout_height="32dp"
android:layout_marginVertical="32dp"
android:layout_marginHorizontal="16dp"
android:background="@drawable/ic_baseline_delete_24"
android:contentDescription="@string/delete"
android:scaleType="fitCenter"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toLeftOf="@id/done_button"
tools:ignore="RtlHardcoded" />

<TextView
android:id="@+id/playback_speed_text"
android:layout_width="wrap_content"
Expand Down
1 change: 1 addition & 0 deletions app/src/main/res/values-b+cs/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@
<string name="capture">Záznam</string>
<string name="play_pause">Přehrát/Zastavit</string>
<string name="done">Hotovo</string>
<string name="delete">Hotova a smazat</string>
<string name="rewind">Skočit na začátek</string>
</resources>
1 change: 1 addition & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,5 +37,6 @@
<string name="capture">Capture</string>
<string name="play_pause">Play/Pause</string>
<string name="done">Done</string>
<string name="delete">Done and delete</string>
<string name="rewind">Rewind to start</string>
</resources>

0 comments on commit f23b0f3

Please sign in to comment.