forked from recloudstream/cloudstream
		
	subtitle offset, fixed #36
This commit is contained in:
		
							parent
							
								
									cd6c79b961
								
							
						
					
					
						commit
						a1a5af6570
					
				
					 9 changed files with 636 additions and 7 deletions
				
			
		|  | @ -85,6 +85,10 @@ abstract class AbstractPlayerFragment( | |||
|         throw NotImplementedError() | ||||
|     } | ||||
| 
 | ||||
|     open fun subtitlesChanged() { | ||||
|         throw NotImplementedError() | ||||
|     } | ||||
| 
 | ||||
|     private fun keepScreenOn(on: Boolean) { | ||||
|         if (on) { | ||||
|             activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) | ||||
|  | @ -325,7 +329,7 @@ abstract class AbstractPlayerFragment( | |||
|                 SKIP_OP_VIDEO_PERCENTAGE, | ||||
|                 PRELOAD_NEXT_EPISODE_PERCENTAGE, | ||||
|                 NEXT_WATCH_EPISODE_PERCENTAGE, | ||||
|             ) | ||||
|             ), subtitlesUpdates = ::subtitlesChanged | ||||
|         ) | ||||
| 
 | ||||
|         if (player is CS3IPlayer) { | ||||
|  |  | |||
|  | @ -74,6 +74,7 @@ class CS3IPlayer : IPlayer { | |||
|     private var updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null | ||||
|     private var requestAutoFocus: (() -> Unit)? = null | ||||
|     private var playerError: ((Exception) -> Unit)? = null | ||||
|     private var subtitlesUpdates: (() -> Unit)? = null | ||||
| 
 | ||||
|     /** width x height */ | ||||
|     private var playerDimensionsLoaded: ((Pair<Int, Int>) -> Unit)? = null | ||||
|  | @ -100,7 +101,8 @@ class CS3IPlayer : IPlayer { | |||
|         requestedListeningPercentages: List<Int>?, | ||||
|         playerPositionChanged: ((Pair<Long, Long>) -> Unit)?, | ||||
|         nextEpisode: (() -> Unit)?, | ||||
|         prevEpisode: (() -> Unit)? | ||||
|         prevEpisode: (() -> Unit)?, | ||||
|         subtitlesUpdates: (() -> Unit)? | ||||
|     ) { | ||||
|         this.playerUpdated = playerUpdated | ||||
|         this.updateIsPlaying = updateIsPlaying | ||||
|  | @ -111,6 +113,7 @@ class CS3IPlayer : IPlayer { | |||
|         this.playerPositionChanged = playerPositionChanged | ||||
|         this.nextEpisode = nextEpisode | ||||
|         this.prevEpisode = prevEpisode | ||||
|         this.subtitlesUpdates = subtitlesUpdates | ||||
|     } | ||||
| 
 | ||||
|     // I know, this is not a perfect solution, however it works for fixing subs | ||||
|  | @ -215,6 +218,17 @@ class CS3IPlayer : IPlayer { | |||
|         } ?: false | ||||
|     } | ||||
| 
 | ||||
|     var currentSubtitleOffset : Long = 0 | ||||
| 
 | ||||
|     override fun setSubtitleOffset(offset: Long) { | ||||
|         currentSubtitleOffset = offset | ||||
|         currentTextRenderer?.setRenderOffsetMs(offset) | ||||
|     } | ||||
| 
 | ||||
|     override fun getSubtitleOffset(): Long { | ||||
|         return currentSubtitleOffset//currentTextRenderer?.getRenderOffsetMs() ?: currentSubtitleOffset | ||||
|     } | ||||
| 
 | ||||
|     override fun getCurrentPreferredSubtitle(): SubtitleData? { | ||||
|         return subtitleHelper.getAllSubtitles().firstOrNull { sub -> | ||||
|             exoPlayerSelectedTracks.any { | ||||
|  | @ -251,6 +265,7 @@ class CS3IPlayer : IPlayer { | |||
| 
 | ||||
|         exoPlayer?.release() | ||||
|         simpleCache?.release() | ||||
|         currentTextRenderer = null | ||||
| 
 | ||||
|         exoPlayer = null | ||||
|         simpleCache = null | ||||
|  | @ -395,7 +410,7 @@ class CS3IPlayer : IPlayer { | |||
|             return trackSelector | ||||
|         } | ||||
| 
 | ||||
|         var currentTextRenderer: TextRenderer? = null | ||||
|         var currentTextRenderer: CustomTextRenderer? = null | ||||
| 
 | ||||
|         private fun buildExoPlayer( | ||||
|             context: Context, | ||||
|  | @ -404,6 +419,7 @@ class CS3IPlayer : IPlayer { | |||
|             currentWindow: Int, | ||||
|             playbackPosition: Long, | ||||
|             playBackSpeed: Float, | ||||
|             subtitleOffset : Long, | ||||
|             playWhenReady: Boolean = true, | ||||
|             cacheFactory: CacheDataSource.Factory? = null, | ||||
|             trackSelector: TrackSelector? = null, | ||||
|  | @ -419,7 +435,8 @@ class CS3IPlayer : IPlayer { | |||
|                             metadataRendererOutput | ||||
|                         ).map { | ||||
|                             if (it is TextRenderer) { | ||||
|                                 currentTextRenderer = TextRenderer( | ||||
|                                 currentTextRenderer = CustomTextRenderer( | ||||
|                                     subtitleOffset, | ||||
|                                     textRendererOutput, | ||||
|                                     eventHandler.looper, | ||||
|                                     CustomSubtitleDecoderFactory() | ||||
|  | @ -534,7 +551,8 @@ class CS3IPlayer : IPlayer { | |||
|                 playbackPosition, | ||||
|                 playBackSpeed, | ||||
|                 playWhenReady = isPlaying, // this keep the current state of the player | ||||
|                 cacheFactory = cacheFactory | ||||
|                 cacheFactory = cacheFactory, | ||||
|                 subtitleOffset = currentSubtitleOffset | ||||
|             ) | ||||
| 
 | ||||
|             requestSubtitleUpdate = ::reloadSubs | ||||
|  | @ -558,6 +576,7 @@ class CS3IPlayer : IPlayer { | |||
|                 override fun onTracksInfoChanged(tracksInfo: TracksInfo) { | ||||
|                     exoPlayerSelectedTracks = | ||||
|                         tracksInfo.trackGroupInfos.mapNotNull { it.trackGroup.getFormat(0).language?.let { lang -> lang to it.isSelected } } | ||||
|                     subtitlesUpdates?.invoke() | ||||
|                     super.onTracksInfoChanged(tracksInfo) | ||||
|                 } | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,30 @@ | |||
| package com.lagradost.cloudstream3.ui.player | ||||
| 
 | ||||
| import android.os.Looper | ||||
| import com.google.android.exoplayer2.text.SubtitleDecoderFactory | ||||
| import com.google.android.exoplayer2.text.TextOutput | ||||
| 
 | ||||
| class CustomTextRenderer( | ||||
|     offset: Long, | ||||
|     output: TextOutput?, | ||||
|     outputLooper: Looper?, | ||||
|     decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT | ||||
| ) : NonFinalTextRenderer(output, outputLooper, decoderFactory) { | ||||
|     private var offsetPositionUs: Long = 0L | ||||
| 
 | ||||
|     init { | ||||
|         setRenderOffsetMs(offset) | ||||
|     } | ||||
| 
 | ||||
|     fun setRenderOffsetMs(offset : Long) { | ||||
|         offsetPositionUs = offset * 1000L | ||||
|     } | ||||
| 
 | ||||
|     fun getRenderOffsetMs() : Long { | ||||
|         return offsetPositionUs / 1000L | ||||
|     } | ||||
| 
 | ||||
|     override fun render( positionUs: Long,  elapsedRealtimeUs: Long) { | ||||
|         super.render(positionUs + offsetPositionUs, elapsedRealtimeUs + offsetPositionUs) | ||||
|     } | ||||
| } | ||||
|  | @ -11,6 +11,7 @@ import android.media.AudioManager | |||
| import android.os.Build | ||||
| import android.os.Bundle | ||||
| import android.provider.Settings | ||||
| import android.text.Editable | ||||
| import android.util.DisplayMetrics | ||||
| import android.view.KeyEvent | ||||
| import android.view.MotionEvent | ||||
|  | @ -20,11 +21,16 @@ import android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHO | |||
| import android.view.animation.AlphaAnimation | ||||
| import android.view.animation.Animation | ||||
| import android.view.animation.AnimationUtils | ||||
| import android.widget.EditText | ||||
| import android.widget.ImageView | ||||
| import android.widget.TextView | ||||
| import androidx.appcompat.app.AlertDialog | ||||
| import androidx.core.graphics.blue | ||||
| import androidx.core.graphics.green | ||||
| import androidx.core.graphics.red | ||||
| import androidx.core.view.isGone | ||||
| import androidx.core.view.isVisible | ||||
| import androidx.core.widget.doOnTextChanged | ||||
| import androidx.preference.PreferenceManager | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.getKey | ||||
| import com.lagradost.cloudstream3.AcraApplication.Companion.setKey | ||||
|  | @ -35,6 +41,7 @@ import com.lagradost.cloudstream3.mvvm.logError | |||
| import com.lagradost.cloudstream3.utils.Qualities | ||||
| import com.lagradost.cloudstream3.utils.SingleSelectionHelper.showDialog | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.colorFromAttribute | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.dismissSafe | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.getNavigationBarHeight | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.getStatusBarHeight | ||||
| import com.lagradost.cloudstream3.utils.UIHelper.hideSystemUI | ||||
|  | @ -71,6 +78,19 @@ open class FullScreenPlayer : AbstractPlayerFragment() { | |||
|     protected var doubleTapEnabled = false | ||||
|     protected var doubleTapPauseEnabled = true | ||||
| 
 | ||||
|     protected var subtitleDelay | ||||
|         set(value) = try { | ||||
|             player.setSubtitleOffset(-value) | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|         } | ||||
|         get() = try { | ||||
|             -player.getSubtitleOffset() | ||||
|         } catch (e: Exception) { | ||||
|             logError(e) | ||||
|             0L | ||||
|         } | ||||
| 
 | ||||
|     //private var useSystemBrightness = false | ||||
|     protected var useTrueSystemBrightness = true | ||||
|     private val fullscreenNotch = true //TODO SETTING | ||||
|  | @ -196,6 +216,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { | |||
|         player_top_holder?.startAnimation(fadeAnimation) | ||||
|     } | ||||
| 
 | ||||
|     override fun subtitlesChanged() { | ||||
|         player_subtitle_offset_btt?.isGone = player.getCurrentPreferredSubtitle() == null | ||||
|     } | ||||
| 
 | ||||
|     override fun onResume() { | ||||
|         activity?.hideSystemUI() | ||||
|         activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE | ||||
|  | @ -241,6 +265,77 @@ open class FullScreenPlayer : AbstractPlayerFragment() { | |||
|         player.seekTime(85000) // skip 85s | ||||
|     } | ||||
| 
 | ||||
|     private fun showSubtitleOffsetDialog() { | ||||
|         context?.let { ctx -> | ||||
|             val builder = | ||||
|                 AlertDialog.Builder(ctx, R.style.AlertDialogCustom) | ||||
|                     .setView(R.layout.subtitle_offset) | ||||
|             val dialog = builder.create() | ||||
|             dialog.show() | ||||
| 
 | ||||
|             val beforeOffset = subtitleDelay | ||||
| 
 | ||||
|             val applyButton = dialog.findViewById<TextView>(R.id.apply_btt)!! | ||||
|             val cancelButton = dialog.findViewById<TextView>(R.id.cancel_btt)!! | ||||
|             val input = dialog.findViewById<EditText>(R.id.subtitle_offset_input)!! | ||||
|             val sub = dialog.findViewById<ImageView>(R.id.subtitle_offset_subtract)!! | ||||
|             val add = dialog.findViewById<ImageView>(R.id.subtitle_offset_add)!! | ||||
|             val subTitle = dialog.findViewById<TextView>(R.id.subtitle_offset_sub_title)!! | ||||
| 
 | ||||
|             input.doOnTextChanged { text, _, _, _ -> | ||||
|                 text?.toString()?.toLongOrNull()?.let { | ||||
|                     subtitleDelay = it | ||||
|                     when { | ||||
|                         it > 0L -> { | ||||
|                             context?.getString(R.string.subtitle_offset_extra_hint_later_format) | ||||
|                                 ?.format(it) | ||||
|                         } | ||||
|                         it < 0L -> { | ||||
|                             context?.getString(R.string.subtitle_offset_extra_hint_before_format) | ||||
|                                 ?.format(-it) | ||||
|                         } | ||||
|                         it == 0L -> { | ||||
|                             context?.getString(R.string.subtitle_offset_extra_hint_none_format) | ||||
|                         } | ||||
|                         else -> { | ||||
|                             null | ||||
|                         } | ||||
|                     }?.let { str -> | ||||
|                         subTitle.text = str | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             input.text = Editable.Factory.getInstance()?.newEditable(beforeOffset.toString()) | ||||
| 
 | ||||
|             val buttonChange = 100L | ||||
| 
 | ||||
|             fun changeBy(by: Long) { | ||||
|                 val current = (input.text?.toString()?.toLongOrNull() ?: 0) + by | ||||
|                 input.text = Editable.Factory.getInstance()?.newEditable(current.toString()) | ||||
|             } | ||||
| 
 | ||||
|             add.setOnClickListener { | ||||
|                 changeBy(buttonChange) | ||||
|             } | ||||
| 
 | ||||
|             sub.setOnClickListener { | ||||
|                 changeBy(-buttonChange) | ||||
|             } | ||||
| 
 | ||||
|             dialog.setOnDismissListener { | ||||
|                 activity?.hideSystemUI() | ||||
|             } | ||||
|             applyButton.setOnClickListener { | ||||
|                 dialog.dismissSafe(activity) | ||||
|                 player.seekTime(1L) | ||||
|             } | ||||
|             cancelButton.setOnClickListener { | ||||
|                 subtitleDelay = beforeOffset | ||||
|                 dialog.dismissSafe(activity) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun showSpeedDialog() { | ||||
|         val speedsText = | ||||
|             listOf( | ||||
|  | @ -1055,6 +1150,10 @@ open class FullScreenPlayer : AbstractPlayerFragment() { | |||
|             toggleLock() | ||||
|         } | ||||
| 
 | ||||
|         player_subtitle_offset_btt?.setOnClickListener { | ||||
|             showSubtitleOffsetDialog() | ||||
|         } | ||||
| 
 | ||||
|         exo_rew?.setOnClickListener { | ||||
|             autoHide() | ||||
|             rewind() | ||||
|  |  | |||
|  | @ -71,6 +71,9 @@ interface IPlayer { | |||
|     fun seekTime(time: Long) | ||||
|     fun seekTo(time: Long) | ||||
| 
 | ||||
|     fun getSubtitleOffset() : Long // in ms | ||||
|     fun setSubtitleOffset(offset : Long) // in ms | ||||
| 
 | ||||
|     fun initCallbacks( | ||||
|         playerUpdated: (Any?) -> Unit,                              // attach player to view | ||||
|         updateIsPlaying: ((Pair<CSPlayerLoading, CSPlayerLoading>) -> Unit)? = null, // (wasPlaying, isPlaying) | ||||
|  | @ -81,6 +84,7 @@ interface IPlayer { | |||
|         playerPositionChanged: ((Pair<Long, Long>) -> Unit)? = null,// (position, duration) this is used to update UI based of the current time | ||||
|         nextEpisode: (() -> Unit)? = null,                          // this is used by the player to load the next episode | ||||
|         prevEpisode: (() -> Unit)? = null,                          // this is used by the player to load the previous episode | ||||
|         subtitlesUpdates: (() -> Unit)? = null,                     // callback from player to inform that subtitles have updated in some way | ||||
|     ) | ||||
| 
 | ||||
|     fun updateSubtitleStyle(style: SaveCaptionStyle) | ||||
|  |  | |||
|  | @ -0,0 +1,369 @@ | |||
| /* | ||||
|  * 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. | ||||
|  */ | ||||
| package com.lagradost.cloudstream3.ui.player | ||||
| 
 | ||||
| import android.os.Handler | ||||
| import android.os.Looper | ||||
| import android.os.Message | ||||
| import androidx.annotation.IntDef | ||||
| import com.google.android.exoplayer2.* | ||||
| import com.google.android.exoplayer2.source.SampleStream.ReadDataResult | ||||
| import com.google.android.exoplayer2.text.* | ||||
| import com.google.android.exoplayer2.util.Assertions | ||||
| import com.google.android.exoplayer2.util.Log | ||||
| import com.google.android.exoplayer2.util.MimeTypes | ||||
| import com.google.android.exoplayer2.util.Util | ||||
| 
 | ||||
| /** | ||||
|  * A renderer for text. | ||||
|  * | ||||
|  * | ||||
|  * [Subtitle]s are decoded from sample data using [SubtitleDecoder] instances | ||||
|  * obtained from a [SubtitleDecoderFactory]. The actual rendering of the subtitle [Cue]s | ||||
|  * is delegated to a [TextOutput]. | ||||
|  */ | ||||
| open class NonFinalTextRenderer @JvmOverloads constructor( | ||||
|     output: TextOutput?, | ||||
|     outputLooper: Looper?, | ||||
|     private val decoderFactory: SubtitleDecoderFactory = SubtitleDecoderFactory.DEFAULT | ||||
| ) : | ||||
|     BaseRenderer(C.TRACK_TYPE_TEXT), Handler.Callback { | ||||
|     @MustBeDocumented | ||||
|     @kotlin.annotation.Retention(AnnotationRetention.SOURCE) | ||||
|     @IntDef( | ||||
|         REPLACEMENT_STATE_NONE, | ||||
|         REPLACEMENT_STATE_SIGNAL_END_OF_STREAM, | ||||
|         REPLACEMENT_STATE_WAIT_END_OF_STREAM | ||||
|     ) | ||||
|     private annotation class ReplacementState | ||||
| 
 | ||||
|     private val outputHandler: Handler? = if (outputLooper == null) null else Util.createHandler( | ||||
|         outputLooper,  /* callback= */ | ||||
|         this | ||||
|     ) | ||||
|     private val output: TextOutput = Assertions.checkNotNull(output) | ||||
|     private val formatHold: FormatHolder = FormatHolder() | ||||
|     private var inputStreamEnded = false | ||||
|     private var outputStreamEnded = false | ||||
|     private var waitingForKeyFrame = false | ||||
| 
 | ||||
|     @ReplacementState | ||||
|     private var decoderReplacementState = 0 | ||||
|     private var streamFormat: Format? = null | ||||
|     private var decoder: SubtitleDecoder? = null | ||||
|     private var nextInputBuffer: SubtitleInputBuffer? = null | ||||
|     private var subtitle: SubtitleOutputBuffer? = null | ||||
|     private var nextSubtitle: SubtitleOutputBuffer? = null | ||||
|     private var nextSubtitleEventIndex = 0 | ||||
|     private var finalStreamEndPositionUs: Long | ||||
|     override fun getName(): String { | ||||
|         return TAG | ||||
|     } | ||||
| 
 | ||||
|     @RendererCapabilities.Capabilities | ||||
|     override fun supportsFormat(format: Format): Int { | ||||
|         return if (decoderFactory.supportsFormat(format)) { | ||||
|             RendererCapabilities.create( | ||||
|                 if (format.cryptoType == C.CRYPTO_TYPE_NONE) C.FORMAT_HANDLED else C.FORMAT_UNSUPPORTED_DRM | ||||
|             ) | ||||
|         } else if (MimeTypes.isText(format.sampleMimeType)) { | ||||
|             RendererCapabilities.create(C.FORMAT_UNSUPPORTED_SUBTYPE) | ||||
|         } else { | ||||
|             RendererCapabilities.create(C.FORMAT_UNSUPPORTED_TYPE) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Sets the position at which to stop rendering the current stream. | ||||
|      * | ||||
|      * | ||||
|      * Must be called after [.setCurrentStreamFinal]. | ||||
|      * | ||||
|      * @param streamEndPositionUs The position to stop rendering at or [C.LENGTH_UNSET] to | ||||
|      * render until the end of the current stream. | ||||
|      */ | ||||
| 
 | ||||
|     override fun onStreamChanged(formats: Array<Format>, startPositionUs: Long, offsetUs: Long) { | ||||
|         streamFormat = formats[0] | ||||
|         if (decoder != null) { | ||||
|             decoderReplacementState = REPLACEMENT_STATE_SIGNAL_END_OF_STREAM | ||||
|         } else { | ||||
|             initDecoder() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onPositionReset(positionUs: Long, joining: Boolean) { | ||||
|         clearOutput() | ||||
|         inputStreamEnded = false | ||||
|         outputStreamEnded = false | ||||
|         finalStreamEndPositionUs = C.TIME_UNSET | ||||
|         if (decoderReplacementState != REPLACEMENT_STATE_NONE) { | ||||
|             replaceDecoder() | ||||
|         } else { | ||||
|             releaseBuffers() | ||||
|             Assertions.checkNotNull(decoder).flush() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun render(positionUs: Long, elapsedRealtimeUs: Long) { | ||||
|         if (isCurrentStreamFinal | ||||
|             && finalStreamEndPositionUs != C.TIME_UNSET && positionUs >= finalStreamEndPositionUs | ||||
|         ) { | ||||
|             releaseBuffers() | ||||
|             outputStreamEnded = true | ||||
|         } | ||||
|         if (outputStreamEnded) { | ||||
|             return | ||||
|         } | ||||
|         if (nextSubtitle == null) { | ||||
|             Assertions.checkNotNull(decoder).setPositionUs(positionUs) | ||||
|             nextSubtitle = try { | ||||
|                 Assertions.checkNotNull(decoder).dequeueOutputBuffer() | ||||
|             } catch (e: SubtitleDecoderException) { | ||||
|                 handleDecoderError(e) | ||||
|                 return | ||||
|             } | ||||
|         } | ||||
|         if (state != STATE_STARTED) { | ||||
|             return | ||||
|         } | ||||
|         var textRendererNeedsUpdate = false | ||||
|         if (subtitle != null) { | ||||
|             // We're iterating through the events in a subtitle. Set textRendererNeedsUpdate if we | ||||
|             // advance to the next event. | ||||
|             var subtitleNextEventTimeUs = nextEventTime | ||||
|             while (subtitleNextEventTimeUs <= positionUs) { | ||||
|                 nextSubtitleEventIndex++ | ||||
|                 subtitleNextEventTimeUs = nextEventTime | ||||
|                 textRendererNeedsUpdate = true | ||||
|             } | ||||
|         } | ||||
|         if (nextSubtitle != null) { | ||||
|             val nextSubtitle = nextSubtitle | ||||
|             if (nextSubtitle!!.isEndOfStream) { | ||||
|                 if (!textRendererNeedsUpdate && nextEventTime == Long.MAX_VALUE) { | ||||
|                     if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { | ||||
|                         replaceDecoder() | ||||
|                     } else { | ||||
|                         releaseBuffers() | ||||
|                         outputStreamEnded = true | ||||
|                     } | ||||
|                 } | ||||
|             } else if (nextSubtitle.timeUs <= positionUs) { | ||||
|                 // Advance to the next subtitle. Sync the next event index and trigger an update. | ||||
|                 if (subtitle != null) { | ||||
|                     subtitle!!.release() | ||||
|                 } | ||||
|                 nextSubtitleEventIndex = nextSubtitle.getNextEventTimeIndex(positionUs) | ||||
|                 subtitle = nextSubtitle | ||||
|                 this.nextSubtitle = null | ||||
|                 textRendererNeedsUpdate = true | ||||
|             } | ||||
|         } | ||||
|         if (textRendererNeedsUpdate) { | ||||
|             // If textRendererNeedsUpdate then subtitle must be non-null. | ||||
|             Assertions.checkNotNull(subtitle) | ||||
|             // textRendererNeedsUpdate is set and we're playing. Update the renderer. | ||||
|             updateOutput(subtitle!!.getCues(positionUs)) | ||||
|         } | ||||
|         if (decoderReplacementState == REPLACEMENT_STATE_WAIT_END_OF_STREAM) { | ||||
|             return | ||||
|         } | ||||
|         try { | ||||
|             while (!inputStreamEnded) { | ||||
|                 var nextInputBuffer = nextInputBuffer | ||||
|                 if (nextInputBuffer == null) { | ||||
|                     nextInputBuffer = Assertions.checkNotNull(decoder).dequeueInputBuffer() | ||||
|                     if (nextInputBuffer == null) { | ||||
|                         return | ||||
|                     } | ||||
|                     this.nextInputBuffer = nextInputBuffer | ||||
|                 } | ||||
|                 if (decoderReplacementState == REPLACEMENT_STATE_SIGNAL_END_OF_STREAM) { | ||||
|                     nextInputBuffer.setFlags(C.BUFFER_FLAG_END_OF_STREAM) | ||||
|                     Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer) | ||||
|                     this.nextInputBuffer = null | ||||
|                     decoderReplacementState = REPLACEMENT_STATE_WAIT_END_OF_STREAM | ||||
|                     return | ||||
|                 } | ||||
|                 // Try and read the next subtitle from the source. | ||||
|                 @ReadDataResult val result = | ||||
|                     readSource(formatHold, nextInputBuffer,  /* readFlags= */0) | ||||
|                 if (result == C.RESULT_BUFFER_READ) { | ||||
|                     if (nextInputBuffer.isEndOfStream) { | ||||
|                         inputStreamEnded = true | ||||
|                         waitingForKeyFrame = false | ||||
|                     } else { | ||||
|                         val format = formatHold.format | ||||
|                             ?: // We haven't received a format yet. | ||||
|                             return | ||||
|                         nextInputBuffer.subsampleOffsetUs = format.subsampleOffsetUs | ||||
|                         nextInputBuffer.flip() | ||||
|                         waitingForKeyFrame = waitingForKeyFrame and !nextInputBuffer.isKeyFrame | ||||
|                     } | ||||
|                     if (!waitingForKeyFrame) { | ||||
|                         Assertions.checkNotNull(decoder).queueInputBuffer(nextInputBuffer) | ||||
|                         this.nextInputBuffer = null | ||||
|                     } | ||||
|                 } else if (result == C.RESULT_NOTHING_READ) { | ||||
|                     return | ||||
|                 } | ||||
|             } | ||||
|         } catch (e: SubtitleDecoderException) { | ||||
|             handleDecoderError(e) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     override fun onDisabled() { | ||||
|         streamFormat = null | ||||
|         finalStreamEndPositionUs = C.TIME_UNSET | ||||
|         clearOutput() | ||||
|         releaseDecoder() | ||||
|     } | ||||
| 
 | ||||
|     override fun isEnded(): Boolean { | ||||
|         return outputStreamEnded | ||||
|     } | ||||
| 
 | ||||
|     override fun isReady(): Boolean { | ||||
|         // Don't block playback whilst subtitles are loading. | ||||
|         // Note: To change this behavior, it will be necessary to consider [Internal: b/12949941]. | ||||
|         return true | ||||
|     } | ||||
| 
 | ||||
|     private fun releaseBuffers() { | ||||
|         nextInputBuffer = null | ||||
|         nextSubtitleEventIndex = C.INDEX_UNSET | ||||
|         if (subtitle != null) { | ||||
|             subtitle!!.release() | ||||
|             subtitle = null | ||||
|         } | ||||
|         if (nextSubtitle != null) { | ||||
|             nextSubtitle!!.release() | ||||
|             nextSubtitle = null | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun releaseDecoder() { | ||||
|         releaseBuffers() | ||||
|         Assertions.checkNotNull(decoder).release() | ||||
|         decoder = null | ||||
|         decoderReplacementState = REPLACEMENT_STATE_NONE | ||||
|     } | ||||
| 
 | ||||
|     private fun initDecoder() { | ||||
|         waitingForKeyFrame = true | ||||
|         decoder = decoderFactory.createDecoder(Assertions.checkNotNull(streamFormat)) | ||||
|     } | ||||
| 
 | ||||
|     private fun replaceDecoder() { | ||||
|         releaseDecoder() | ||||
|         initDecoder() | ||||
|     } | ||||
| 
 | ||||
|     private val nextEventTime: Long | ||||
|         get() { | ||||
|             if (nextSubtitleEventIndex == C.INDEX_UNSET) { | ||||
|                 return Long.MAX_VALUE | ||||
|             } | ||||
|             Assertions.checkNotNull(subtitle) | ||||
|             return if (nextSubtitleEventIndex >= subtitle!!.eventTimeCount) Long.MAX_VALUE else subtitle!!.getEventTime( | ||||
|                 nextSubtitleEventIndex | ||||
|             ) | ||||
|         } | ||||
| 
 | ||||
|     private fun updateOutput(cues: List<Cue>) { | ||||
|         if (outputHandler != null) { | ||||
|             outputHandler.obtainMessage(MSG_UPDATE_OUTPUT, cues).sendToTarget() | ||||
|         } else { | ||||
|             invokeUpdateOutputInternal(cues) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun clearOutput() { | ||||
|         updateOutput(emptyList()) | ||||
|     } | ||||
| 
 | ||||
|     override fun handleMessage(msg: Message): Boolean { | ||||
|         return when (msg.what) { | ||||
|             MSG_UPDATE_OUTPUT -> { | ||||
|                 invokeUpdateOutputInternal(msg.obj as List<Cue>) | ||||
|                 true | ||||
|             } | ||||
|             else -> throw IllegalStateException() | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private fun invokeUpdateOutputInternal(cues: List<Cue>) { | ||||
|         output.onCues(cues) | ||||
|     } | ||||
| 
 | ||||
|     /** | ||||
|      * Called when [.decoder] throws an exception, so it can be logged and playback can | ||||
|      * continue. | ||||
|      * | ||||
|      * | ||||
|      * Logs `e` and resets state to allow decoding the next sample. | ||||
|      */ | ||||
|     private fun handleDecoderError(e: SubtitleDecoderException) { | ||||
|         Log.e( | ||||
|             TAG, | ||||
|             "Subtitle decoding failed. streamFormat=$streamFormat", e | ||||
|         ) | ||||
|         clearOutput() | ||||
|         replaceDecoder() | ||||
|     } | ||||
| 
 | ||||
|     companion object { | ||||
|         private const val TAG = "TextRenderer" | ||||
| 
 | ||||
|         /** The decoder does not need to be replaced.  */ | ||||
|         private const val REPLACEMENT_STATE_NONE = 0 | ||||
| 
 | ||||
|         /** | ||||
|          * The decoder needs to be replaced, but we haven't yet signaled an end of stream to the existing | ||||
|          * decoder. We need to do so in order to ensure that it outputs any remaining buffers before we | ||||
|          * release it. | ||||
|          */ | ||||
|         private const val REPLACEMENT_STATE_SIGNAL_END_OF_STREAM = 1 | ||||
| 
 | ||||
|         /** | ||||
|          * The decoder needs to be replaced, and we've signaled an end of stream to the existing decoder. | ||||
|          * We're waiting for the decoder to output an end of stream signal to indicate that it has output | ||||
|          * any remaining buffers before we release it. | ||||
|          */ | ||||
|         private const val REPLACEMENT_STATE_WAIT_END_OF_STREAM = 2 | ||||
|         private const val MSG_UPDATE_OUTPUT = 0 | ||||
|     } | ||||
|     /** | ||||
|      * @param output The output. | ||||
|      * @param outputLooper The looper associated with the thread on which the output should be called. | ||||
|      * If the output makes use of standard Android UI components, then this should normally be the | ||||
|      * looper associated with the application's main thread, which can be obtained using [     ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called | ||||
|      * directly on the player's internal rendering thread. | ||||
|      * @param decoderFactory A factory from which to obtain [SubtitleDecoder] instances. | ||||
|      */ | ||||
|     /** | ||||
|      * @param output The output. | ||||
|      * @param outputLooper The looper associated with the thread on which the output should be called. | ||||
|      * If the output makes use of standard Android UI components, then this should normally be the | ||||
|      * looper associated with the application's main thread, which can be obtained using [     ][android.app.Activity.getMainLooper]. Null may be passed if the output should be called | ||||
|      * directly on the player's internal rendering thread. | ||||
|      */ | ||||
|     init { | ||||
|         finalStreamEndPositionUs = C.TIME_UNSET | ||||
|     } | ||||
| } | ||||
|  | @ -508,15 +508,26 @@ | |||
|                                 style="@style/VideoButton" | ||||
|                                 android:nextFocusLeft="@id/player_resize_btt" | ||||
| 
 | ||||
|                                 android:nextFocusRight="@id/player_sources_btt" | ||||
|                                 android:nextFocusRight="@id/player_subtitle_offset_btt" | ||||
|                                 app:icon="@drawable/ic_baseline_speed_24" | ||||
|                                 tools:text="Speed" /> | ||||
| 
 | ||||
|                         <com.google.android.material.button.MaterialButton | ||||
|                                 android:id="@+id/player_sources_btt" | ||||
|                                 android:visibility="gone" | ||||
|                                 tools:visibility="visible" | ||||
|                                 android:id="@+id/player_subtitle_offset_btt" | ||||
|                                 style="@style/VideoButton" | ||||
|                                 android:nextFocusLeft="@id/player_speed_btt" | ||||
| 
 | ||||
|                                 android:nextFocusRight="@id/player_sources_btt" | ||||
|                                 app:icon="@drawable/ic_outline_subtitles_24" | ||||
|                                 android:text="@string/subtitle_offset"/> | ||||
| 
 | ||||
|                         <com.google.android.material.button.MaterialButton | ||||
|                                 android:id="@+id/player_sources_btt" | ||||
|                                 style="@style/VideoButton" | ||||
|                                 android:nextFocusLeft="@id/player_subtitle_offset_btt" | ||||
| 
 | ||||
|                                 android:nextFocusRight="@id/player_skip_op" | ||||
|                                 android:text="@string/video_source" | ||||
|                                 app:icon="@drawable/ic_baseline_playlist_play_24" /> | ||||
|  |  | |||
							
								
								
									
										86
									
								
								app/src/main/res/layout/subtitle_offset.xml
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								app/src/main/res/layout/subtitle_offset.xml
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,86 @@ | |||
| <?xml version="1.0" encoding="utf-8"?> | ||||
| <LinearLayout xmlns:tools="http://schemas.android.com/tools" | ||||
|         android:orientation="vertical" xmlns:android="http://schemas.android.com/apk/res/android" | ||||
|         android:layout_width="300dp" | ||||
|         android:layout_height="match_parent" | ||||
|         xmlns:app="http://schemas.android.com/apk/res-auto"> | ||||
| 
 | ||||
|     <TextView | ||||
|             style="@style/WatchHeaderText" | ||||
|             android:layout_margin="0dp" | ||||
|             android:paddingTop="10dp" | ||||
|             android:text="@string/subtitle_offset_title" | ||||
|             android:layout_gravity="center" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" /> | ||||
| 
 | ||||
|     <TextView | ||||
|             android:id="@+id/subtitle_offset_sub_title" | ||||
|             android:layout_gravity="center" | ||||
|             tools:text="@string/subtitle_offset_extra_hint_none_format" | ||||
|             android:textColor="?attr/grayTextColor" | ||||
|             android:layout_width="wrap_content" | ||||
|             android:layout_height="wrap_content" /> | ||||
|     <LinearLayout | ||||
|             android:orientation="horizontal" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="wrap_content"> | ||||
| 
 | ||||
|         <ImageView | ||||
|                 android:id="@+id/subtitle_offset_subtract" | ||||
|                 android:background="?android:attr/selectableItemBackgroundBorderless" | ||||
|                 android:padding="10dp" | ||||
|                 android:layout_weight="1" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:src="@drawable/baseline_remove_24" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="match_parent" | ||||
|                 app:tint="?attr/white" | ||||
|                 tools:ignore="ContentDescription" /> | ||||
|         <EditText | ||||
|                 android:layout_weight="20" | ||||
|                 android:id="@+id/subtitle_offset_input" | ||||
|                 android:inputType="numberSigned" | ||||
|                 android:layout_width="0dp" | ||||
|                 android:layout_height="wrap_content" | ||||
|                 android:autofillHints="@string/subtitle_offset_hint" | ||||
|                 tools:ignore="LabelFor" /> | ||||
| 
 | ||||
|         <ImageView | ||||
|                 android:id="@+id/subtitle_offset_add" | ||||
|                 android:background="?android:attr/selectableItemBackgroundBorderless" | ||||
|                 android:padding="10dp" | ||||
|                 android:layout_weight="1" | ||||
|                 android:layout_gravity="center" | ||||
|                 android:src="@drawable/ic_baseline_add_24" | ||||
|                 android:layout_width="wrap_content" | ||||
|                 android:layout_height="match_parent" | ||||
|                 app:tint="?attr/white" | ||||
|                 tools:ignore="ContentDescription" /> | ||||
|     </LinearLayout> | ||||
|     <LinearLayout | ||||
|             android:orientation="horizontal" | ||||
|             android:layout_gravity="bottom" | ||||
|             android:gravity="bottom|end" | ||||
|             android:layout_width="match_parent" | ||||
|             android:layout_height="60dp"> | ||||
| 
 | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 style="@style/WhiteButton" | ||||
|                 android:layout_gravity="center_vertical|end" | ||||
|                 android:visibility="visible" | ||||
|                 android:text="@string/sort_apply" | ||||
|                 android:id="@+id/apply_btt" | ||||
|                 android:layout_width="wrap_content"> | ||||
| 
 | ||||
|             <requestFocus /> | ||||
|         </com.google.android.material.button.MaterialButton> | ||||
| 
 | ||||
|         <com.google.android.material.button.MaterialButton | ||||
|                 style="@style/BlackButton" | ||||
|                 android:layout_gravity="center_vertical|end" | ||||
|                 android:text="@string/sort_cancel" | ||||
|                 android:id="@+id/cancel_btt" | ||||
|                 android:layout_width="wrap_content" /> | ||||
|     </LinearLayout> | ||||
| </LinearLayout> | ||||
|  | @ -377,6 +377,13 @@ | |||
|     <string name="subtitles_depressed">Depressed</string> | ||||
|     <string name="subtitles_shadow">Shadow</string> | ||||
|     <string name="subtitles_raised">Raised</string> | ||||
|     <string name="subtitle_offset">Sync subs</string> | ||||
|     <string name="subtitle_offset_hint">1000ms</string> | ||||
|     <string name="subtitle_offset_title">Subtitle delay</string> | ||||
|     <string name="subtitle_offset_extra_hint_later_format">Use this if the subtitles are shown %dms too early</string> | ||||
|     <string name="subtitle_offset_extra_hint_before_format">Use this if subtitles are shown %dms too late</string> | ||||
|     <string name="subtitle_offset_extra_hint_none_format">No subtitle delay</string> | ||||
| 
 | ||||
|     <!-- | ||||
|     Example text (pangram) can optionally be translated; if you do, include all the letters in the alphabet, | ||||
|     see:  | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue