feat: button to screenshot/capture current frame

This commit is contained in:
Bnyro 2024-07-29 16:13:30 +02:00
parent f086b3ac4a
commit 52fb2a9861
4 changed files with 73 additions and 7 deletions

View File

@ -8,18 +8,23 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Bitmap
import android.media.session.PlaybackState import android.media.session.PlaybackState
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.os.PowerManager import android.os.PowerManager
import android.view.KeyEvent import android.view.KeyEvent
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.PixelCopy
import android.view.SurfaceView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams import android.view.ViewGroup.LayoutParams
import android.widget.Toast import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.constraintlayout.motion.widget.MotionLayout import androidx.constraintlayout.motion.widget.MotionLayout
import androidx.constraintlayout.motion.widget.TransitionAdapter import androidx.constraintlayout.motion.widget.TransitionAdapter
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
@ -118,6 +123,7 @@ import java.util.concurrent.Executors
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.ceil import kotlin.math.ceil
@androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class) @androidx.annotation.OptIn(androidx.media3.common.util.UnstableApi::class)
class PlayerFragment : Fragment(), OnlinePlayerOptions { class PlayerFragment : Fragment(), OnlinePlayerOptions {
private var _binding: FragmentPlayerBinding? = null private var _binding: FragmentPlayerBinding? = null
@ -352,6 +358,21 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
) )
private var screenshotBitmap: Bitmap? = null
private val openScreenshotFile =
registerForActivityResult(ActivityResultContracts.CreateDocument("image/png")) { uri ->
if (uri == null) {
screenshotBitmap = null
return@registerForActivityResult
}
context?.contentResolver?.openOutputStream(uri)?.use { outputStream ->
screenshotBitmap?.compress(Bitmap.CompressFormat.PNG, 100, outputStream)
}
screenshotBitmap = null
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val playerData = requireArguments().parcelable<PlayerData>(IntentData.playerData)!! val playerData = requireArguments().parcelable<PlayerData>(IntentData.playerData)!!
@ -526,9 +547,12 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
activity?.supportFragmentManager activity?.supportFragmentManager
?.setFragmentResultListener(CommentsSheet.HANDLE_LINK_REQUEST_KEY, viewLifecycleOwner) { _, bundle -> ?.setFragmentResultListener(
CommentsSheet.HANDLE_LINK_REQUEST_KEY,
viewLifecycleOwner
) { _, bundle ->
bundle.getString(IntentData.url)?.let { handleLink(it) } bundle.getString(IntentData.url)?.let { handleLink(it) }
} }
binding.commentsToggle.setOnClickListener { binding.commentsToggle.setOnClickListener {
if (!this::streams.isInitialized) return@setOnClickListener if (!this::streams.isInitialized) return@setOnClickListener
@ -628,6 +652,28 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
} }
} }
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
binding.relPlayerScreenshot.setOnClickListener {
if (!this::exoPlayer.isInitialized || !this::streams.isInitialized) return@setOnClickListener
val surfaceView =
binding.player.videoSurfaceView as? SurfaceView ?: return@setOnClickListener
val bmp = Bitmap.createBitmap(
surfaceView.width,
surfaceView.height,
Bitmap.Config.ARGB_8888
)
PixelCopy.request(surfaceView, bmp, { _ ->
screenshotBitmap = bmp
val currentPosition = exoPlayer.currentPosition.toFloat() / 1000
openScreenshotFile.launch("${streams.title}-${currentPosition}.png")
}, Handler(Looper.getMainLooper()))
}
} else {
binding.relPlayerScreenshot.isGone = true
}
binding.playerChannel.setOnClickListener { binding.playerChannel.setOnClickListener {
if (!this::streams.isInitialized) return@setOnClickListener if (!this::streams.isInitialized) return@setOnClickListener
@ -702,7 +748,8 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
binding.player.updateMarginsByFullscreenMode() binding.player.updateMarginsByFullscreenMode()
// set status bar icon color back to theme color after fullscreen dialog closed! // set status bar icon color back to theme color after fullscreen dialog closed!
windowInsetsControllerCompat.isAppearanceLightStatusBars = !ThemeHelper.isDarkMode(requireContext()) windowInsetsControllerCompat.isAppearanceLightStatusBars =
!ThemeHelper.isDarkMode(requireContext())
} }
/** /**
@ -1031,13 +1078,17 @@ class PlayerFragment : Fragment(), OnlinePlayerOptions {
// close comment bottom sheet if opened for next video // close comment bottom sheet if opened for next video
activity?.supportFragmentManager?.fragments?.filterIsInstance<CommentsSheet>() activity?.supportFragmentManager?.fragments?.filterIsInstance<CommentsSheet>()
?.firstOrNull()?.dismiss() ?.firstOrNull()?.dismiss()
} }
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
private fun initializePlayerView() { private fun initializePlayerView() {
// initialize the player view actions // initialize the player view actions
binding.player.initialize(doubleTapOverlayBinding, playerGestureControlsViewBinding, chaptersViewModel) binding.player.initialize(
doubleTapOverlayBinding,
playerGestureControlsViewBinding,
chaptersViewModel
)
binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this) binding.player.initPlayerOptions(viewModel, viewLifecycleOwner, trackSelector, this)
binding.descriptionLayout.setStreams(streams) binding.descriptionLayout.setStreams(streams)

View File

@ -78,6 +78,12 @@
style="@style/PlayerActionsButton" style="@style/PlayerActionsButton"
android:text="@string/pip" android:text="@string/pip"
app:icon="@drawable/ic_open" /> app:icon="@drawable/ic_open" />
<com.google.android.material.button.MaterialButton
android:id="@+id/relPlayer_screenshot"
style="@style/PlayerActionsButton"
android:text="@string/screenshot"
app:icon="@drawable/ic_copy" />
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>
@ -206,7 +212,8 @@
app:layout_constraintEnd_toEndOf="@id/main_container" app:layout_constraintEnd_toEndOf="@id/main_container"
app:layout_constraintStart_toStartOf="@id/main_container" app:layout_constraintStart_toStartOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container" app:layout_constraintTop_toTopOf="@id/main_container"
app:show_buffering="when_playing"> app:show_buffering="when_playing"
app:surface_type="surface_view">
<com.github.libretube.ui.views.DoubleTapOverlay <com.github.libretube.ui.views.DoubleTapOverlay
android:id="@+id/doubleTapOverlay" android:id="@+id/doubleTapOverlay"

View File

@ -78,6 +78,12 @@
style="@style/PlayerActionsButton" style="@style/PlayerActionsButton"
android:text="@string/pip" android:text="@string/pip"
app:icon="@drawable/ic_open" /> app:icon="@drawable/ic_open" />
<com.google.android.material.button.MaterialButton
android:id="@+id/relPlayer_screenshot"
style="@style/PlayerActionsButton"
android:text="@string/screenshot"
app:icon="@drawable/ic_copy" />
</LinearLayout> </LinearLayout>
</HorizontalScrollView> </HorizontalScrollView>
@ -179,7 +185,8 @@
app:layout_constraintBottom_toBottomOf="@id/main_container" app:layout_constraintBottom_toBottomOf="@id/main_container"
app:layout_constraintStart_toStartOf="@id/main_container" app:layout_constraintStart_toStartOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container" app:layout_constraintTop_toTopOf="@id/main_container"
app:show_buffering="when_playing"> app:show_buffering="when_playing"
app:surface_type="surface_view">
<com.github.libretube.ui.views.DoubleTapOverlay <com.github.libretube.ui.views.DoubleTapOverlay
android:id="@+id/doubleTapOverlay" android:id="@+id/doubleTapOverlay"

View File

@ -463,6 +463,7 @@
<string name="default_language">Default</string> <string name="default_language">Default</string>
<string name="behavior_when_minimized">Behavior when minimized</string> <string name="behavior_when_minimized">Behavior when minimized</string>
<string name="external_player">External player</string> <string name="external_player">External player</string>
<string name="screenshot">Screenshot</string>
<!-- Backup & Restore Settings --> <!-- Backup & Restore Settings -->
<string name="import_subscriptions_from">Import subscriptions from</string> <string name="import_subscriptions_from">Import subscriptions from</string>