Skip to content

Commit

Permalink
Merge pull request #65 from damontecres/feat/seek-thumbnails
Browse files Browse the repository at this point in the history
Show preview thumbnails when scrubbing
  • Loading branch information
damontecres authored Jan 25, 2024
2 parents e2b0afe + bbbc1f1 commit 9eea1e8
Show file tree
Hide file tree
Showing 9 changed files with 285 additions and 3 deletions.
3 changes: 3 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,9 @@ dependencies {
implementation("androidx.media3:media3-ui:$mediaVersion")
implementation("androidx.media3:media3-exoplayer-dash:$mediaVersion")
implementation("androidx.media3:media3-exoplayer-hls:$mediaVersion")
implementation("com.github.rubensousa:previewseekbar:3.1.1")
implementation("com.github.rubensousa:previewseekbar-media3:1.1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")

testImplementation("androidx.test:core-ktx:1.5.0")
testImplementation("junit:junit:4.13.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ package com.github.damontecres.stashapp
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageView
import androidx.annotation.OptIn
import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.media3.common.MediaItem
Expand All @@ -19,6 +21,9 @@ import androidx.media3.ui.PlayerView
import androidx.preference.PreferenceManager
import com.github.damontecres.stashapp.data.Scene
import com.github.damontecres.stashapp.util.Constants
import com.github.damontecres.stashapp.util.StashPreviewLoader
import com.github.rubensousa.previewseekbar.PreviewBar
import com.github.rubensousa.previewseekbar.media3.PreviewTimeBar

@OptIn(UnstableApi::class)
class PlaybackExoFragment :
Expand All @@ -27,6 +32,8 @@ class PlaybackExoFragment :
private var player: ExoPlayer? = null
private lateinit var scene: Scene
private lateinit var videoView: PlayerView
private lateinit var previewTimeBar: PreviewTimeBar
private lateinit var controlsLayout: ConstraintLayout

private var playbackPosition = -1L

Expand All @@ -35,6 +42,8 @@ class PlaybackExoFragment :
override fun hideControlsIfVisible(): Boolean {
if (videoView.isControllerFullyVisible) {
videoView.hideController()
previewTimeBar.hidePreview()
previewTimeBar.hideScrubber(250L)
return true
}
return false
Expand Down Expand Up @@ -139,6 +148,8 @@ class PlaybackExoFragment :
) {
super.onViewCreated(view, savedInstanceState)

controlsLayout = view.findViewById(R.id.player_controls)

scene = requireActivity().intent.getParcelableExtra(DetailsActivity.MOVIE) as Scene?
?: throw RuntimeException()

Expand Down Expand Up @@ -186,13 +197,46 @@ class PlaybackExoFragment :
androidx.media3.ui.R.id.exo_prev,
androidx.media3.ui.R.id.exo_play_pause,
androidx.media3.ui.R.id.exo_next,
androidx.media3.ui.R.id.exo_rew,
androidx.media3.ui.R.id.exo_ffwd,
)
buttons.forEach {
view.findViewById<View>(it)?.onFocusChangeListener = onFocusChangeListener
}

// val timeBar: DefaultTimeBar =
// view.findViewById(androidx.media3.ui.R.id.exo_progress)
val previewImageView = view.findViewById<ImageView>(R.id.video_preview_image_view)
previewTimeBar = view.findViewById(R.id.exo_progress)

if (scene.spriteUrl != null) {
previewTimeBar.isPreviewEnabled = true
previewTimeBar.setPreviewLoader(
StashPreviewLoader(
requireContext(),
previewImageView,
scene,
),
)
previewTimeBar.addOnScrubListener(
object : PreviewBar.OnScrubListener {
override fun onScrubStart(previewBar: PreviewBar) {
player!!.playWhenReady = false
}

override fun onScrubMove(
previewBar: PreviewBar,
progress: Int,
fromUser: Boolean,
) {
}

override fun onScrubStop(previewBar: PreviewBar) {
player!!.playWhenReady = true
}
},
)
} else {
previewTimeBar.isPreviewEnabled = false
}
}

@OptIn(UnstableApi::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ data class Scene(
val studioId: String?,
val studioName: String?,
val streams: Map<String, String>,
val spriteUrl: String?,
val duration: Double?,
val resumeTime: Double?,
val videoCodec: String?,
Expand Down Expand Up @@ -50,6 +51,7 @@ data class Scene(
studioId = data.studio?.id,
studioName = data.studio?.name,
streams = streams,
spriteUrl = data.paths.sprite,
duration = fileData?.duration,
resumeTime = data.resume_time,
videoCodec = fileData?.video_codec,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ class QueryEngine(private val context: Context, private val showToasts: Boolean
fun updateFilter(filter: FindFilterType?): FindFilterType? {
return if (filter != null) {
if (filter.sort.getOrNull()?.startsWith("random_") == true) {
Log.v(TAG, "Updating random filter")
filter.copy(sort = Optional.present("random_" + Random.nextInt(1e8.toInt())))
} else {
filter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package com.github.damontecres.stashapp.util

import android.content.Context
import android.graphics.Bitmap
import android.util.Log
import android.widget.ImageView
import com.bumptech.glide.Glide
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
import com.bumptech.glide.load.resource.bitmap.BitmapTransformation
import com.bumptech.glide.request.target.Target
import com.github.damontecres.stashapp.data.Scene
import com.github.rubensousa.previewseekbar.PreviewLoader
import java.nio.ByteBuffer
import java.security.MessageDigest

class StashPreviewLoader(
private val context: Context,
private val imageView: ImageView,
private val scene: Scene,
) : PreviewLoader {
override fun loadPreview(
currentPosition: Long,
max: Long,
) {
Log.d(TAG, "loadPreview: currentPosition=$currentPosition")
Glide.with(context)
.load(createGlideUrl(scene.spriteUrl!!, context))
.override(Target.SIZE_ORIGINAL, Target.SIZE_ORIGINAL)
// .override(
// GlideThumbnailTransformation.IMAGE_WIDTH * GlideThumbnailTransformation.MAX_COLUMNS,
// GlideThumbnailTransformation.IMAGE_HEIGHT * GlideThumbnailTransformation.MAX_LINES
// )
.transform(
GlideThumbnailTransformation(
(scene.duration!! * 1000).toLong(),
currentPosition,
),
)
.into(imageView)
}

companion object {
private const val TAG = "StashPreviewLoader"
}

class GlideThumbnailTransformation(duration: Long, position: Long) : BitmapTransformation() {
private val x: Int
private val y: Int

init {
val square = position / (duration / (MAX_LINES * MAX_COLUMNS))
y = square.toInt() / MAX_LINES
x = square.toInt() % MAX_COLUMNS
}

override fun transform(
pool: BitmapPool,
toTransform: Bitmap,
outWidth: Int,
outHeight: Int,
): Bitmap {
val width = toTransform.width / MAX_COLUMNS
val height = toTransform.height / MAX_LINES
return Bitmap.createBitmap(toTransform, x * width, y * height, width, height)
}

override fun updateDiskCacheKey(messageDigest: MessageDigest) {
val data = ByteBuffer.allocate(8).putInt(x).putInt(y).array()
messageDigest.update(data)
}

override fun equals(o: Any?): Boolean {
if (this === o) return true
if (o == null || javaClass != o.javaClass) return false
val that = o as GlideThumbnailTransformation
return if (x != that.x) false else y == that.y
}

override fun hashCode(): Int {
var result = x
result = 31 * result + y
return result
}

companion object {
const val MAX_LINES = 9
const val MAX_COLUMNS = 9
const val IMAGE_WIDTH = 160
const val IMAGE_HEIGHT = IMAGE_WIDTH * 9 / 16
private const val THUMBNAILS_EACH = 5000 // milliseconds
}
}
}
10 changes: 10 additions & 0 deletions app/src/main/res/drawable/video_frame.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">

<stroke
android:width="@dimen/video_frame_width"
android:color="@android:color/white" />

<solid android:color="@android:color/black" />
</shape>
126 changes: 126 additions & 0 deletions app/src/main/res/layout/exoplayer_controls.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?xml version="1.0" encoding="utf-8"?><!-- Copyright (C) 2016 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/player_controls"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:orientation="vertical">

<LinearLayout
android:id="@+id/controlsLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CC000000"
android:gravity="center"
android:orientation="horizontal"
app:layout_constraintBottom_toBottomOf="parent">

<ImageButton
android:id="@id/exo_prev"
style="@style/ExoMediaButton.Previous" />

<ImageButton
android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind" />

<ImageButton
android:id="@id/exo_play_pause"
style="@style/ExoMediaButton.Play" />

<ImageButton
android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward" />

<ImageButton
android:id="@id/exo_next"
style="@style/ExoMediaButton.Next" />

</LinearLayout>

<TextView
android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="8dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBottom_toTopOf="@id/controlsLayout"
app:layout_constraintStart_toStartOf="parent"
tools:text="18:20" />

<com.github.rubensousa.previewseekbar.media3.PreviewTimeBar
android:id="@+id/exo_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintBottom_toBottomOf="@id/exo_position"
app:layout_constraintEnd_toStartOf="@id/exo_duration"
app:layout_constraintStart_toEndOf="@+id/exo_position"
app:layout_constraintTop_toTopOf="@+id/exo_position"
app:previewAnimationEnabled="true"
app:previewFrameLayout="@id/previewFrameLayout"
app:scrubber_dragged_size="24dp"
app:scrubber_enabled_size="16dp"
app:played_color="@color/selected_background"
app:auto_show="true"
app:hide_on_touch="true"
app:animation_enabled="true"
app:previewAutoHide="true" />

<TextView
android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layout_margin="8dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"
android:textSize="12sp"
android:textStyle="bold"
app:layout_constraintBaseline_toBaselineOf="@id/exo_position"
app:layout_constraintEnd_toEndOf="parent"
tools:text="25:23" />

<FrameLayout
android:id="@+id/previewFrameLayout"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:layout_marginBottom="16dp"
android:background="@drawable/video_frame"
android:visibility="invisible"
app:layout_constraintBottom_toTopOf="@+id/exo_progress"
app:layout_constraintDimensionRatio="16:9"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintWidth_default="percent"
app:layout_constraintWidth_percent="0.35"
tools:visibility="visible">

<ImageView
android:id="@+id/video_preview_image_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="@dimen/video_frame_width"
android:scaleType="fitXY" />
</FrameLayout>

</androidx.constraintlayout.widget.ConstraintLayout>
4 changes: 3 additions & 1 deletion app/src/main/res/layout/video_playback.xml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
app:hide_on_touch="true"
app:animation_enabled="true"
app:scrubber_dragged_size="22dp"
app:played_color="@color/selected_background" />
app:played_color="@color/selected_background"
app:controller_layout_id="@layout/exoplayer_controls"
app:use_controller="true" />

</FrameLayout>
1 change: 1 addition & 0 deletions app/src/main/res/values/dimens.xml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<resources xmlns:tools="http://schemas.android.com/tools">
<integer tools:override="true" name="lb_details_description_body_max_lines">18</integer>
<dimen name="video_frame_width">2dp</dimen>
</resources>

0 comments on commit 9eea1e8

Please sign in to comment.