Last active
December 10, 2018 17:57
-
-
Save pankaj89/4a811e47cb04ffaa046b725718cabe7e to your computer and use it in GitHub Desktop.
Exoplayer - Simple ExoPlayerHelper
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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' | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) {} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() { | |
} | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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