Exoplayer - Simple ExoPlayerHelper
<?xml version="1.0" encoding="utf-8"?> | |
<android.support.constraint.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:layout_width="match_parent" | |
android:layout_height="match_parent" | |
tools:context=".MainActivity"> | |
<com.google.android.exoplayer2.ui.PlayerView | |
android:id="@+id/playerView" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:controller_layout_id="@layout/custom_exo_controller_layout" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" /> | |
<com.master.exoplayersample.VideoTimelineView | |
android:id="@+id/range_slider" | |
android:layout_width="match_parent" | |
android:layout_height="40dp" | |
android:layout_marginStart="8dp" | |
android:layout_marginLeft="8dp" | |
android:layout_marginTop="8dp" | |
android:layout_marginEnd="8dp" | |
android:layout_marginRight="8dp" | |
android:layout_marginBottom="8dp" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintHorizontal_bias="0.0" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toBottomOf="@+id/tvMessage" | |
app:layout_constraintVertical_bias="0.102" /> | |
<TextView | |
android:id="@+id/tvMessage" | |
android:layout_width="0dp" | |
android:layout_height="wrap_content" | |
android:layout_marginStart="8dp" | |
android:layout_marginLeft="8dp" | |
android:layout_marginTop="8dp" | |
android:layout_marginEnd="8dp" | |
android:layout_marginRight="8dp" | |
android:layout_marginBottom="8dp" | |
android:text="TextView" | |
android:gravity="center" | |
android:textColor="@color/colorAccent" | |
app:layout_constraintBottom_toBottomOf="parent" | |
app:layout_constraintEnd_toEndOf="parent" | |
app:layout_constraintStart_toStartOf="parent" | |
app:layout_constraintTop_toTopOf="parent" | |
app:layout_constraintVertical_bias="0.0" | |
tools:text="This is textview" /> | |
</android.support.constraint.ConstraintLayout> |
package com.master.exoplayersample; | |
import android.content.Context; | |
public class AndroidUtilities { | |
public static float density = 1; | |
public static void init(Context context) { | |
density = context.getResources().getDisplayMetrics().density; | |
} | |
public static int dp(float value) { | |
if (value == 0) { | |
return 0; | |
} | |
return (int) Math.ceil(density * value); | |
} | |
public static int dp2(float value) { | |
if (value == 0) { | |
return 0; | |
} | |
return (int) Math.floor(density * value); | |
} | |
} |
apply plugin: 'com.android.application' | |
apply plugin: 'kotlin-android' | |
apply plugin: 'kotlin-android-extensions' | |
apply plugin: 'kotlin-kapt' | |
android { | |
compileSdkVersion 28 | |
defaultConfig { | |
applicationId "com.master.exoplayersample" | |
minSdkVersion 15 | |
targetSdkVersion 28 | |
versionCode 1 | |
versionName "1.0" | |
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" | |
} | |
buildTypes { | |
release { | |
minifyEnabled false | |
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' | |
} | |
} | |
compileOptions { | |
sourceCompatibility 1.8 | |
targetCompatibility 1.8 | |
} | |
dataBinding{ | |
enabled true | |
} | |
} | |
dependencies { | |
implementation fileTree(dir: 'libs', include: ['*.jar']) | |
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" | |
implementation 'com.android.support:appcompat-v7:28.0.0' | |
implementation 'com.android.support.constraint:constraint-layout:1.1.3' | |
testImplementation 'junit:junit:4.12' | |
androidTestImplementation 'com.android.support.test:runner:1.0.2' | |
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' | |
implementation 'com.google.android.exoplayer:exoplayer:2.9.2' | |
} |
<?xml version="1.0" encoding="utf-8"?> | |
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" | |
android:layout_width="match_parent" | |
android:layout_height="match_parent"> | |
<ImageButton | |
android:id="@id/exo_play" | |
style="@style/ExoMediaButton.Play" | |
android:layout_width="100dp" | |
android:layout_height="100dp" | |
android:layout_gravity="center" | |
android:background="#CC000000" /> | |
<ImageButton | |
android:id="@id/exo_pause" | |
style="@style/ExoMediaButton.Pause" | |
android:layout_width="100dp" | |
android:layout_height="100dp" | |
android:layout_gravity="center" | |
android:background="#CC000000" /> | |
</FrameLayout> |
package com.master.exoplayersample | |
import android.arch.lifecycle.Lifecycle | |
import android.arch.lifecycle.LifecycleObserver | |
import android.arch.lifecycle.OnLifecycleEvent | |
import android.net.Uri | |
import android.os.Handler | |
import android.support.v7.app.AppCompatActivity | |
import android.util.Log | |
import android.view.View | |
import com.fanclips.R | |
import com.google.android.exoplayer2.* | |
import com.google.android.exoplayer2.source.ClippingMediaSource | |
import com.google.android.exoplayer2.source.ConcatenatingMediaSource | |
import com.google.android.exoplayer2.source.ExtractorMediaSource | |
import com.google.android.exoplayer2.source.MediaSource | |
import com.google.android.exoplayer2.source.dash.DashMediaSource | |
import com.google.android.exoplayer2.source.hls.HlsMediaSource | |
import com.google.android.exoplayer2.source.smoothstreaming.SsMediaSource | |
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector | |
import com.google.android.exoplayer2.ui.PlayerView | |
import com.google.android.exoplayer2.upstream.* | |
import com.google.android.exoplayer2.upstream.cache.* | |
import com.google.android.exoplayer2.util.Util | |
import java.io.File | |
class ExoPlayerHelper(val mContext: AppCompatActivity, private val playerView: PlayerView, enableCache: Boolean = true) : LifecycleObserver { | |
private var mDataSourceFactory: DataSource.Factory | |
private var mPlayer: SimpleExoPlayer | |
var cacheSizeInMb: Long = 500 | |
private var simpleCache: SimpleCache? = null | |
init { | |
//For lifecycle | |
mContext.lifecycle.addObserver(this) | |
val bandwidthMeter = DefaultBandwidthMeter() | |
mDataSourceFactory = DefaultDataSourceFactory(mContext, Util.getUserAgent(mContext, mContext.getString(R.string.application_name)), bandwidthMeter) | |
// LoadControl that controls when the MediaSource buffers more media, and how much media is buffered. | |
// LoadControl is injected when the player is created. | |
val builder = DefaultLoadControl.Builder(); | |
builder.setAllocator(DefaultAllocator(true, 2 * 1024 * 1024)); | |
builder.setBufferDurationsMs(5000, 5000, 5000, 5000); | |
builder.setPrioritizeTimeOverSizeThresholds(true); | |
val mLoadControl = builder.createDefaultLoadControl(); | |
if (enableCache) { | |
val evictor = LeastRecentlyUsedCacheEvictor(cacheSizeInMb * 1024 * 1024) | |
val file = File(mContext.getCacheDir(), "media") | |
if (simpleCache == null) { | |
simpleCache = SimpleCache(file, evictor) | |
} | |
mDataSourceFactory = CacheDataSourceFactory( | |
simpleCache, | |
mDataSourceFactory, | |
FileDataSourceFactory(), | |
CacheDataSinkFactory(simpleCache, (2 * 1024 * 1024).toLong()), | |
CacheDataSource.FLAG_BLOCK_ON_CACHE or CacheDataSource.FLAG_IGNORE_CACHE_ON_ERROR, | |
object : CacheDataSource.EventListener { | |
override fun onCacheIgnored(reason: Int) { | |
Log.d("ZAQ", "onCacheIgnored") | |
} | |
override fun onCachedBytesRead(cacheSizeBytes: Long, cachedBytesRead: Long) { | |
Log.d("ZAQ", "onCachedBytesRead , cacheSizeBytes: $cacheSizeBytes cachedBytesRead: $cachedBytesRead") | |
} | |
}) | |
} | |
mPlayer = ExoPlayerFactory.newSimpleInstance( | |
mContext, | |
DefaultRenderersFactory(mContext), | |
DefaultTrackSelector(), | |
mLoadControl); | |
playerView.player = mPlayer | |
} | |
private var mediaSource: MediaSource? = null | |
private var isPreparing = false //This flag is used only for callback | |
/** | |
* Sets the url to play | |
* | |
* @param url url to play | |
* @param autoPlay whether url will play as soon it Loaded/Prepared | |
*/ | |
fun setUrl(url: String, autoPlay: Boolean = false) { | |
mediaSource = buildMediaSource(Uri.parse(url)) | |
mPlayer.playWhenReady = autoPlay | |
isPreparing = true | |
mPlayer.prepare(mediaSource) | |
} | |
/** | |
* Sets the url to play | |
* | |
* @param urls url to play | |
* @param autoPlay whether url will play as soon it Loaded/Prepared | |
*/ | |
fun setUrls(urls: ArrayList<String>, autoPlay: Boolean = false) { | |
val concatenationMediaSource = ConcatenatingMediaSource(); | |
urls.forEach { | |
concatenationMediaSource.addMediaSource(buildMediaSource(Uri.parse(it))) | |
} | |
mPlayer.playWhenReady = autoPlay | |
isPreparing = true | |
mPlayer.prepare(mediaSource) | |
} | |
/** | |
* Trim or clip media to given start and end milliseconds, | |
* Ensure you must call this method after [setUrl] method call | |
* You Make sure start time < end time ( Something you do :) ) | |
* | |
* @param start starting time in millisecond | |
* @param end ending time in millisecond | |
*/ | |
fun clip(start: Long, end: Long) { | |
if (mediaSource != null) { | |
mediaSource = ClippingMediaSource(mediaSource, start * 1000, end * 1000) | |
} | |
mPlayer.prepare(mediaSource) | |
} | |
private fun buildMediaSource(uri: Uri): MediaSource { | |
val type = Util.inferContentType(uri) | |
when (type) { | |
C.TYPE_SS -> return SsMediaSource.Factory(mDataSourceFactory).createMediaSource(uri) | |
C.TYPE_DASH -> return DashMediaSource.Factory(mDataSourceFactory).createMediaSource(uri) | |
C.TYPE_HLS -> return HlsMediaSource.Factory(mDataSourceFactory).createMediaSource(uri) | |
C.TYPE_OTHER -> return ExtractorMediaSource.Factory(mDataSourceFactory).createMediaSource(uri) | |
else -> { | |
throw IllegalStateException("Unsupported type: $type") | |
} | |
} | |
} | |
/** | |
* Used to start player | |
* Ensure you must call this method after [setUrl] method call | |
*/ | |
fun play() { | |
mPlayer.playWhenReady = true | |
} | |
/** | |
* Used to pause player | |
* Ensure you must call this method after [setUrl] method call | |
*/ | |
fun pause() { | |
mPlayer.playWhenReady = false | |
} | |
/** | |
* Used to stop player | |
* Ensure you must call this method after [setUrl] method call | |
*/ | |
fun stop() { | |
mPlayer.stop() | |
} | |
/** | |
* Used to seek player to given position(in milliseconds) | |
* Ensure you must call this method after [setUrl] method call | |
*/ | |
fun seekTo(positionMs: Long) { | |
mPlayer.seekTo(positionMs) | |
} | |
val durationHandler = Handler() | |
private var durationRunnable: Runnable? = null | |
fun startTimer() { | |
if (durationRunnable != null) | |
durationHandler.postDelayed(durationRunnable, 500) | |
} | |
fun stopTimer() { | |
if (durationRunnable != null) | |
durationHandler.removeCallbacks(durationRunnable) | |
} | |
/** | |
* Returns SimpleExoPlayer instance you can use it for your own implementation | |
*/ | |
fun getPlayer(): SimpleExoPlayer { | |
return mPlayer | |
} | |
/** | |
* Used to set different quality url of existing video/audio | |
*/ | |
fun setQualityUrl(qualityUrl: String) { | |
val currentPosition = mPlayer.currentPosition | |
mediaSource = buildMediaSource(Uri.parse(qualityUrl)) | |
mPlayer.prepare(mediaSource) | |
mPlayer.seekTo(currentPosition) | |
} | |
/** | |
* Normal speed is 1f and double the speed would be 2f. | |
*/ | |
fun setSpeed(speed: Float) { | |
val param = PlaybackParameters(speed); | |
mPlayer.setPlaybackParameters(param) | |
} | |
//Life Cycle | |
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE) | |
private fun onPause() { | |
// simpleCache?.release() | |
mPlayer.playWhenReady = false | |
} | |
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY) | |
private fun onDestroy() { | |
simpleCache?.release() | |
mPlayer.playWhenReady = false | |
} | |
//LISTENERS | |
/** | |
* Listener that used for most popular callbacks | |
*/ | |
var listener: Listener? = null | |
set(value) { | |
mPlayer.addListener(object : Player.EventListener { | |
override fun onPlayerError(error: ExoPlaybackException?) { | |
value?.onError(error) | |
} | |
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { | |
if (isPreparing && playbackState == Player.STATE_READY) { | |
isPreparing = false | |
value?.onPlayerReady() | |
} | |
if (playbackState == Player.STATE_BUFFERING) { | |
value?.onBuffering(true) | |
} else { | |
value?.onBuffering(false) | |
} | |
if (playWhenReady) { | |
startTimer() | |
value?.onStart() | |
} else { | |
stopTimer() | |
value?.onStop() | |
} | |
} | |
}) | |
playerView.setControllerVisibilityListener { visibility -> | |
value?.onToggleControllerVisible(visibility == View.VISIBLE) | |
} | |
durationRunnable = Runnable { | |
value?.onProgress(mPlayer.currentPosition) | |
if (mPlayer.playWhenReady) { | |
durationHandler.postDelayed(durationRunnable, 500) | |
} | |
} | |
} | |
interface Listener { | |
fun onPlayerReady() {} | |
fun onStart() {} | |
fun onStop() {} | |
fun onProgress(positionMs: Long) {} | |
fun onError(error: ExoPlaybackException?) {} | |
fun onBuffering(isBuffering: Boolean) {} | |
fun onToggleControllerVisible(isVisible: Boolean) {} | |
} | |
} |
package com.master.exoplayersample | |
import android.os.Bundle | |
import android.support.v7.app.AppCompatActivity | |
import android.util.Log | |
import com.google.android.exoplayer2.ExoPlaybackException | |
import kotlinx.android.synthetic.main.activity_main.* | |
import kotlin.math.roundToLong | |
class MainActivity : AppCompatActivity() { | |
var leftProgress: Float = 0f | |
var rightProgress: Float = 0f | |
lateinit var exoPlayerHelper: ExoPlayerHelper | |
override fun onCreate(savedInstanceState: Bundle?) { | |
super.onCreate(savedInstanceState) | |
setContentView(R.layout.activity_main) | |
AndroidUtilities.init(this) | |
// val file="https://download.blender.org/peach/bigbuckbunny_movies/BigBuckBunny_320x180.mp4" | |
val file = "/storage/emulated/0/WhatsApp/Media/WhatsApp Video/VID-20181209-WA0001.mp4" | |
exoPlayerHelper = ExoPlayerHelper(this, playerView, enableCache = false) | |
exoPlayerHelper.setUrl(file, autoPlay = false) | |
// exoPlayerHelper.clip(21000L, 41000L) | |
// exoPlayerHelper.seekTo(21000L) | |
// exoPlayerHelper.setSpeed(2.5f) | |
// exoPlayerHelper.play() | |
// exoPlayerHelper.pause() | |
// exoPlayerHelper.stop() | |
// exoPlayerHelper.getCurrentPosition() | |
// exoPlayerHelper.getPlayer() | |
// exoPlayerHelper.setQualityUrl("") | |
val TAG = "TAG" | |
exoPlayerHelper.listener = object : ExoPlayerHelper.Listener { | |
override fun onProgress(positionMs: Long) { | |
super.onProgress(positionMs) | |
Log.d(TAG, "onProgress $positionMs") | |
if (positionMs >= rightProgress) { | |
exoPlayerHelper.pause() | |
} | |
} | |
override fun onPlayerReady() { | |
Log.d(TAG, "onPlayerReady") | |
} | |
override fun onBuffering(isBuffering: Boolean) { | |
Log.d(TAG, "onBuffering: ${isBuffering}") | |
} | |
override fun onError(error: ExoPlaybackException?) { | |
Log.d(TAG, "onError: ${error}") | |
} | |
override fun onStart() { | |
super.onStart() | |
Log.d(TAG, "onStart") | |
} | |
override fun onStop() { | |
super.onStop() | |
Log.d(TAG, "onStop") | |
exoPlayerHelper.seekTo(leftProgress.roundToLong()) | |
} | |
override fun onToggleControllerVisible(isVisible: Boolean) { | |
Log.d(TAG, "onToggleControllerVisible:${isVisible}") | |
} | |
} | |
//------Trimmer | |
range_slider.setVideoPath(file) | |
range_slider.setMaxProgressDiffInSec(200f) | |
range_slider.setMinProgressDiffInSec(2f) | |
leftProgress = range_slider.leftProgressInSec | |
rightProgress = range_slider.rightProgressInSec | |
tvMessage.setText("$leftProgress-$rightProgress") | |
exoPlayerHelper.seekTo(leftProgress.roundToLong()) | |
// range_slider.setMaxProgressDiff(0.5f) | |
// range_slider.setMinProgressDiff(0.2f) | |
// range_slider.setRoundFrames(true) | |
range_slider.setDelegate(object : VideoTimelineView.VideoTimelineViewDelegate { | |
override fun onLeftProgressChanged(progress: Float) { | |
leftProgress = range_slider.leftProgressInSec | |
rightProgress = range_slider.rightProgressInSec | |
tvMessage.setText("$leftProgress-$rightProgress") | |
exoPlayerHelper.seekTo(leftProgress.roundToLong()) | |
} | |
override fun onRightProgressChanged(progress: Float) { | |
leftProgress = range_slider.leftProgressInSec | |
rightProgress = range_slider.rightProgressInSec | |
tvMessage.setText("$leftProgress-$rightProgress") | |
exoPlayerHelper.seekTo(leftProgress.roundToLong()) | |
} | |
override fun didStartDragging() { | |
} | |
override fun didStopDragging() { | |
} | |
}) | |
} | |
} |
// Top-level build file where you can add configuration options common to all sub-projects/modules. | |
buildscript { | |
ext.kotlin_version = '1.2.61' | |
repositories { | |
google() | |
jcenter() | |
maven { url "https://jitpack.io" } | |
} | |
dependencies { | |
classpath 'com.android.tools.build:gradle:3.2.0' | |
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" | |
// NOTE: Do not place your application dependencies here; they belong | |
// in the individual module build.gradle files | |
} | |
} | |
allprojects { | |
repositories { | |
google() | |
jcenter() | |
maven { url "https://jitpack.io" } | |
} | |
} | |
task clean(type: Delete) { | |
delete rootProject.buildDir | |
} |
package com.master.exoplayersample; | |
import android.content.Context; | |
import android.graphics.Bitmap; | |
import android.graphics.Canvas; | |
import android.graphics.Paint; | |
import android.graphics.Rect; | |
import android.media.MediaMetadataRetriever; | |
import android.os.AsyncTask; | |
import android.util.AttributeSet; | |
import android.view.MotionEvent; | |
import android.view.View; | |
import java.util.ArrayList; | |
public class VideoTimelineView extends View { | |
private long videoLength = 0; | |
private float progressLeft; | |
private float progressRight = 1; | |
private Paint paint; | |
private Paint paint2; | |
private boolean pressedLeft; | |
private boolean pressedRight; | |
private float pressDx; | |
private MediaMetadataRetriever mediaMetadataRetriever; | |
private VideoTimelineViewDelegate delegate; | |
private ArrayList<Bitmap> frames = new ArrayList<>(); | |
private AsyncTask<Integer, Integer, Bitmap> currentTask; | |
private static final Object sync = new Object(); | |
private long frameTimeOffset; | |
private int frameWidth; | |
private int frameHeight; | |
private int framesToLoad; | |
private float maxProgressDiff = 1.0f; | |
private float minProgressDiff = 0.0f; | |
private boolean isRoundFrames; | |
private Rect rect1; | |
private Rect rect2; | |
public interface VideoTimelineViewDelegate { | |
void onLeftProgressChanged(float progress); | |
void onRightProgressChanged(float progress); | |
void didStartDragging(); | |
void didStopDragging(); | |
} | |
public VideoTimelineView(Context context) { | |
super(context); | |
paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
paint.setColor(0xffffffff); | |
paint2 = new Paint(); | |
paint2.setColor(0x7f000000); | |
} | |
public VideoTimelineView(Context context, AttributeSet attrs) { | |
super(context, attrs); | |
paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
paint.setColor(0xffffffff); | |
paint2 = new Paint(); | |
paint2.setColor(0x7f000000); | |
} | |
public VideoTimelineView(Context context, AttributeSet attrs, int defStyleAttr) { | |
super(context, attrs, defStyleAttr); | |
paint = new Paint(Paint.ANTI_ALIAS_FLAG); | |
paint.setColor(0xffffffff); | |
paint2 = new Paint(); | |
paint2.setColor(0x7f000000); | |
} | |
//seconds | |
public float getLeftProgressInSec() { | |
return videoLength * progressLeft; | |
} | |
public float getRightProgressInSec() { | |
return videoLength * progressRight; | |
} | |
public void setMinProgressDiffInSec(float valueInSec) { | |
setMinProgressDiff((valueInSec*1000) / videoLength); | |
} | |
public void setMaxProgressDiffInSec(float valueInSec) { | |
setMaxProgressDiff((valueInSec*1000) / videoLength); | |
} | |
//seconds end | |
public float getLeftProgress() { | |
return progressLeft; | |
} | |
public float getRightProgress() { | |
return progressRight; | |
} | |
public void setMinProgressDiff(float value) { | |
minProgressDiff = value; | |
} | |
public void setMaxProgressDiff(float value) { | |
maxProgressDiff = value; | |
if (progressRight - progressLeft > maxProgressDiff) { | |
progressRight = progressLeft + maxProgressDiff; | |
invalidate(); | |
} | |
} | |
public void setRoundFrames(boolean value) { | |
isRoundFrames = value; | |
if (isRoundFrames) { | |
rect1 = new Rect(AndroidUtilities.dp(14), AndroidUtilities.dp(14), AndroidUtilities.dp(14 + 28), AndroidUtilities.dp(14 + 28)); | |
rect2 = new Rect(); | |
} | |
} | |
@Override | |
public boolean onTouchEvent(MotionEvent event) { | |
if (event == null) { | |
return false; | |
} | |
float x = event.getX(); | |
float y = event.getY(); | |
int width = getMeasuredWidth() - AndroidUtilities.dp(32); | |
int startX = (int) (width * progressLeft) + AndroidUtilities.dp(16); | |
int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); | |
if (event.getAction() == MotionEvent.ACTION_DOWN) { | |
getParent().requestDisallowInterceptTouchEvent(true); | |
if (mediaMetadataRetriever == null) { | |
return false; | |
} | |
int additionWidth = AndroidUtilities.dp(15); | |
if (startX - additionWidth <= x && x <= startX + additionWidth && y >= 0 && y <= getMeasuredHeight()) { | |
if (delegate != null) { | |
delegate.didStartDragging(); | |
} | |
pressedLeft = true; | |
pressDx = (int) (x - startX); | |
invalidate(); | |
return true; | |
} else if (endX - additionWidth <= x && x <= endX + additionWidth && y >= 0 && y <= getMeasuredHeight()) { | |
if (delegate != null) { | |
delegate.didStartDragging(); | |
} | |
pressedRight = true; | |
pressDx = (int) (x - endX); | |
invalidate(); | |
return true; | |
} | |
} else if (event.getAction() == MotionEvent.ACTION_UP || event.getAction() == MotionEvent.ACTION_CANCEL) { | |
if (pressedLeft) { | |
if (delegate != null) { | |
delegate.didStopDragging(); | |
} | |
pressedLeft = false; | |
return true; | |
} else if (pressedRight) { | |
if (delegate != null) { | |
delegate.didStopDragging(); | |
} | |
pressedRight = false; | |
return true; | |
} | |
} else if (event.getAction() == MotionEvent.ACTION_MOVE) { | |
if (pressedLeft) { | |
startX = (int) (x - pressDx); | |
if (startX < AndroidUtilities.dp(16)) { | |
startX = AndroidUtilities.dp(16); | |
} else if (startX > endX) { | |
startX = endX; | |
} | |
progressLeft = (float) (startX - AndroidUtilities.dp(16)) / (float) width; | |
if (progressRight - progressLeft > maxProgressDiff) { | |
progressRight = progressLeft + maxProgressDiff; | |
} else if (minProgressDiff != 0 && progressRight - progressLeft < minProgressDiff) { | |
progressLeft = progressRight - minProgressDiff; | |
if (progressLeft < 0) { | |
progressLeft = 0; | |
} | |
} | |
if (delegate != null) { | |
delegate.onLeftProgressChanged(progressLeft); | |
} | |
invalidate(); | |
return true; | |
} else if (pressedRight) { | |
endX = (int) (x - pressDx); | |
if (endX < startX) { | |
endX = startX; | |
} else if (endX > width + AndroidUtilities.dp(16)) { | |
endX = width + AndroidUtilities.dp(16); | |
} | |
progressRight = (float) (endX - AndroidUtilities.dp(16)) / (float) width; | |
if (progressRight - progressLeft > maxProgressDiff) { | |
progressLeft = progressRight - maxProgressDiff; | |
} else if (minProgressDiff != 0 && progressRight - progressLeft < minProgressDiff) { | |
progressRight = progressLeft + minProgressDiff; | |
if (progressRight > 1.0f) { | |
progressRight = 1.0f; | |
} | |
} | |
if (delegate != null) { | |
delegate.onRightProgressChanged(progressRight); | |
} | |
invalidate(); | |
return true; | |
} | |
} | |
return false; | |
} | |
public void setColor(int color) { | |
paint.setColor(color); | |
} | |
public void setVideoPath(String path) { | |
destroy(); | |
mediaMetadataRetriever = new MediaMetadataRetriever(); | |
progressLeft = 0.0f; | |
progressRight = 1.0f; | |
try { | |
mediaMetadataRetriever.setDataSource(path); | |
String duration = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); | |
videoLength = Long.parseLong(duration); | |
} catch (Exception e) { | |
} | |
invalidate(); | |
} | |
public void setDelegate(VideoTimelineViewDelegate delegate) { | |
this.delegate = delegate; | |
} | |
private void reloadFrames(int frameNum) { | |
if (mediaMetadataRetriever == null) { | |
return; | |
} | |
if (frameNum == 0) { | |
if (isRoundFrames) { | |
frameHeight = frameWidth = AndroidUtilities.dp(56); | |
framesToLoad = (int) Math.ceil((getMeasuredWidth() - AndroidUtilities.dp(16)) / (frameHeight / 2.0f)); | |
} else { | |
frameHeight = getMeasuredHeight() - AndroidUtilities.dp(5); | |
framesToLoad = (getMeasuredWidth() - AndroidUtilities.dp(16)) / frameHeight; | |
frameWidth = (int) Math.ceil((float) (getMeasuredWidth() - AndroidUtilities.dp(16)) / (float) framesToLoad); | |
} | |
frameTimeOffset = videoLength / framesToLoad; | |
} | |
currentTask = new AsyncTask<Integer, Integer, Bitmap>() { | |
private int frameNum = 0; | |
@Override | |
protected Bitmap doInBackground(Integer... objects) { | |
frameNum = objects[0]; | |
Bitmap bitmap = null; | |
if (isCancelled()) { | |
return null; | |
} | |
try { | |
bitmap = mediaMetadataRetriever.getFrameAtTime(frameTimeOffset * frameNum * 1000, MediaMetadataRetriever.OPTION_CLOSEST_SYNC); | |
if (isCancelled()) { | |
return null; | |
} | |
if (bitmap != null) { | |
Bitmap result = Bitmap.createBitmap(frameWidth, frameHeight, bitmap.getConfig()); | |
Canvas canvas = new Canvas(result); | |
float scaleX = (float) frameWidth / (float) bitmap.getWidth(); | |
float scaleY = (float) frameHeight / (float) bitmap.getHeight(); | |
float scale = scaleX > scaleY ? scaleX : scaleY; | |
int w = (int) (bitmap.getWidth() * scale); | |
int h = (int) (bitmap.getHeight() * scale); | |
Rect srcRect = new Rect(0, 0, bitmap.getWidth(), bitmap.getHeight()); | |
Rect destRect = new Rect((frameWidth - w) / 2, (frameHeight - h) / 2, w, h); | |
canvas.drawBitmap(bitmap, srcRect, destRect, null); | |
bitmap.recycle(); | |
bitmap = result; | |
} | |
} catch (Exception e) { | |
} | |
return bitmap; | |
} | |
@Override | |
protected void onPostExecute(Bitmap bitmap) { | |
if (!isCancelled()) { | |
frames.add(bitmap); | |
invalidate(); | |
if (frameNum < framesToLoad) { | |
reloadFrames(frameNum + 1); | |
} | |
} | |
} | |
}; | |
currentTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, frameNum, null, null); | |
} | |
public void destroy() { | |
synchronized (sync) { | |
try { | |
if (mediaMetadataRetriever != null) { | |
mediaMetadataRetriever.release(); | |
mediaMetadataRetriever = null; | |
} | |
} catch (Exception e) { | |
} | |
} | |
for (int a = 0; a < frames.size(); a++) { | |
Bitmap bitmap = frames.get(a); | |
if (bitmap != null) { | |
bitmap.recycle(); | |
} | |
} | |
frames.clear(); | |
if (currentTask != null) { | |
currentTask.cancel(true); | |
currentTask = null; | |
} | |
} | |
public void clearFrames() { | |
for (int a = 0; a < frames.size(); a++) { | |
Bitmap bitmap = frames.get(a); | |
if (bitmap != null) { | |
bitmap.recycle(); | |
} | |
} | |
frames.clear(); | |
if (currentTask != null) { | |
currentTask.cancel(true); | |
currentTask = null; | |
} | |
invalidate(); | |
} | |
@Override | |
protected void onDraw(Canvas canvas) { | |
int width = getMeasuredWidth() - AndroidUtilities.dp(36); | |
int startX = (int) (width * progressLeft) + AndroidUtilities.dp(16); | |
int endX = (int) (width * progressRight) + AndroidUtilities.dp(16); | |
canvas.save(); | |
canvas.clipRect(AndroidUtilities.dp(16), 0, width + AndroidUtilities.dp(20), getMeasuredHeight()); | |
if (frames.isEmpty() && currentTask == null) { | |
reloadFrames(0); | |
} else { | |
int offset = 0; | |
for (int a = 0; a < frames.size(); a++) { | |
Bitmap bitmap = frames.get(a); | |
if (bitmap != null) { | |
int x = AndroidUtilities.dp(16) + offset * (isRoundFrames ? frameWidth / 2 : frameWidth); | |
int y = AndroidUtilities.dp(2); | |
if (isRoundFrames) { | |
rect2.set(x, y, x + AndroidUtilities.dp(28), y + AndroidUtilities.dp(28)); | |
canvas.drawBitmap(bitmap, rect1, rect2, null); | |
} else { | |
canvas.drawBitmap(bitmap, x, y, null); | |
} | |
} | |
offset++; | |
} | |
} | |
int top = AndroidUtilities.dp(2); | |
canvas.drawRect(AndroidUtilities.dp(16), top, startX, getMeasuredHeight() - top, paint2); | |
canvas.drawRect(endX + AndroidUtilities.dp(4), top, AndroidUtilities.dp(16) + width + AndroidUtilities.dp(4), getMeasuredHeight() - top, paint2); | |
canvas.drawRect(startX, 0, startX + AndroidUtilities.dp(2), getMeasuredHeight(), paint); | |
canvas.drawRect(endX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), getMeasuredHeight(), paint); | |
canvas.drawRect(startX + AndroidUtilities.dp(2), 0, endX + AndroidUtilities.dp(4), top, paint); | |
canvas.drawRect(startX + AndroidUtilities.dp(2), getMeasuredHeight() - top, endX + AndroidUtilities.dp(4), getMeasuredHeight(), paint); | |
canvas.restore(); | |
canvas.drawCircle(startX + AndroidUtilities.dp(1), getMeasuredHeight() / 2, AndroidUtilities.dp(7), paint); | |
canvas.drawCircle(endX + AndroidUtilities.dp(3), getMeasuredHeight() / 2, AndroidUtilities.dp(7), paint); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment