Hello friends, i am Yogesh Paliyal. I recently want to use Video View in my application with Custom Buttons of play, pause, full screen, etc.
There are Many libraries which provide VideoView with many features, but due to some problem they are not working as planned.
Here is some Libraries example which i used you can also try that it if that meets your requirements.
1. Universal VideoView
So i found the solution, creating my own custom VideoView.
In this we require some resources
1. Play icon (ic_play.xml).
2. Pause icon (ic_pause.xml).
3. Portrait icon (ic_fullscreen_exit.xml).
4. Landscape icon (ic_fullscreen.xml ).
So lets begin the tutorial.
First go through the flow.
Instead of videoView we use SurfaceView to create our Custom VideoView & bind media player to the SurfaceView.
Step 1: Design your media controller.(Play, Pause fullscreen buttons & Seek bar for progress.)
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#CC000000"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center_vertical">
<ImageButton android:id="@+id/pause"
style="@android:style/MediaButton.Play"
/>
<TextView android:id="@+id/time_current"
android:textSize="14sp"
android:textStyle="bold"
android:paddingTop="4dip"
android:paddingLeft="4dip"
android:textColor="@android:color/white"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingRight="4dip" />
<SeekBar
android:id="@+id/mediacontroller_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="0dip"
android:layout_weight="1"
android:layout_height="32dip" />
<TextView android:id="@+id/time"
android:textSize="14sp"
android:textStyle="bold"
android:paddingTop="4dip"
android:paddingRight="4dip"
android:textColor="@android:color/white"
android:layout_gravity="center_horizontal|center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:paddingLeft="4dip" />
<ImageButton
android:id="@+id/fullscreen"
android:paddingTop="4dip"
android:paddingBottom="4dip"
android:paddingLeft="10dip"
android:paddingRight="4dip"
android:layout_gravity="center_vertical"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@android:color/transparent"/>
</LinearLayout>
</LinearLayout>
Step 2.
Create Custom MediaController for our video player to show the options.
import android.content.Context
import android.util.AttributeSet
import android.util.Log
import java.util.Formatter
import java.util.Locale
/*
* Copyright (C) 2006 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.
*/
import android.os.Handler
import android.os.Message
import android.view.Gravity
import android.view.KeyEvent
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
import android.widget.ImageButton
import android.widget.ProgressBar
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import java.lang.ref.WeakReference
/**
* A view containing controls for a MediaPlayer. Typically contains the
* buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress
* slider. It takes care of synchronizing the controls with the state
* of the MediaPlayer.
*
*
* The way to use this class is to instantiate it programatically.
* The MediaController will create a default set of controls
* and put them in a window floating above your application. Specifically,
* the controls will float above the view specified with setAnchorView().
* The window will disappear if left idle for three seconds and reappear
* when the user touches the anchor view.
*
*
* Functions like show() and hide() have no effect when MediaController
* is created in an xml layout.
*
* MediaController will hide and
* show the buttons according to these rules:
*
* * The "previous" and "next" buttons are hidden until setPrevNextListeners()
* has been called
* * The "previous" and "next" buttons are visible but disabled if
* setPrevNextListeners() was called with null listeners
* * The "rewind" and "fastforward" buttons are shown unless requested
* otherwise by using the MediaController(Context, boolean) constructor
* with the boolean set to false
*
*/
class MyMediaController : FrameLayout {
private var mPlayer: MediaPlayerControl? = null
private var mContext: Context? = null
private var mAnchor: ViewGroup? = null
private var mRoot: View? = null
private var mProgress: ProgressBar? = null
private var mEndTime: TextView? = null
private var mCurrentTime: TextView? = null
var isShowing: Boolean = false
private set
private var mDragging: Boolean = false
private var mUseFastForward: Boolean = false
private var mFromXml: Boolean = false
private var mListenersSet: Boolean = false
private var mNextListener: View.OnClickListener? = null
private var mPrevListener: View.OnClickListener? = null
var mFormatBuilder: StringBuilder ?= null
var mFormatter: Formatter ?= null
private var mPauseButton: ImageButton? = null
private var mFfwdButton: ImageButton? = null
private var mRewButton: ImageButton? = null
private var mNextButton: ImageButton? = null
private var mPrevButton: ImageButton? = null
private var mFullscreenButton: ImageButton? = null
private val mHandler = MessageHandler(this)
private val mPauseListener = OnClickListener {
doPauseResume()
show(sDefaultTimeout)
}
private val mFullscreenListener = OnClickListener {
doToggleFullscreen()
show(sDefaultTimeout)
}
// There are two scenarios that can trigger the seekbar listener to trigger:
//
// The first is the user using the touchpad to adjust the posititon of the
// seekbar's thumb. In this case onStartTrackingTouch is called followed by
// a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
// We're setting the field "mDragging" to true for the duration of the dragging
// session to avoid jumps in the position in case of ongoing playback.
//
// The second scenario involves the user operating the scroll ball, in this
// case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
// we will simply apply the updated position without suspending regular updates.
private val mSeekListener = object : OnSeekBarChangeListener {
override fun onStartTrackingTouch(bar: SeekBar) {
show(3600000)
mDragging = true
// By removing these pending progress messages we make sure
// that a) we won't update the progress while the user adjusts
// the seekbar and b) once the user is done dragging the thumb
// we will post one of these messages to the queue again and
// this ensures that there will be exactly one message queued up.
mHandler.removeMessages(SHOW_PROGRESS)
}
override fun onProgressChanged(bar: SeekBar, progress: Int, fromuser: Boolean) {
if (mPlayer == null) {
return
}
if (!fromuser) {
// We're not interested in programmatically generated changes to
// the progress bar's position.
return
}
val duration = mPlayer!!.getDuration().toLong()
val newposition = duration * progress / 1000L
mPlayer!!.seekTo(newposition.toInt())
if (mCurrentTime != null)
mCurrentTime!!.text = stringForTime(newposition.toInt())
}
override fun onStopTrackingTouch(bar: SeekBar) {
mDragging = false
setProgress()
updatePausePlay()
show(sDefaultTimeout)
// Ensure that progress is properly updated in the future,
// the call to show() does not guarantee this because it is a
// no-op if we are already showing.
mHandler.sendEmptyMessage(SHOW_PROGRESS)
}
}
private val mRewListener = OnClickListener {
if (mPlayer == null) {
return@OnClickListener
}
var pos = mPlayer!!.getCurrentPosition()
pos -= 5000 // milliseconds
mPlayer!!.seekTo(pos)
setProgress()
show(sDefaultTimeout)
}
private val mFfwdListener = OnClickListener {
if (mPlayer == null) {
return@OnClickListener
}
var pos = mPlayer!!.getCurrentPosition()
pos += 15000 // milliseconds
mPlayer!!.seekTo(pos)
setProgress()
show(sDefaultTimeout)
}
constructor(context: Context, attrs: AttributeSet) : super(context, attrs) {
mRoot = null
mContext = context
mUseFastForward = true
mFromXml = true
Log.i(TAG, TAG)
}
constructor(context: Context, useFastForward: Boolean) : super(context) {
mContext = context
mUseFastForward = useFastForward
Log.i(TAG, TAG)
}
constructor(context: Context) : this(context, true) {
Log.i(TAG, TAG)
}
public override fun onFinishInflate() {
super.onFinishInflate()
if (mRoot != null)
initControllerView(mRoot!!)
}
fun setMediaPlayer(player: MediaPlayerControl) {
mPlayer = player
updatePausePlay()
updateFullScreen()
}
/**
* Set the view that acts as the anchor for the control view.
* This can for example be a VideoView, or your Activity's main view.
* @param view The view to which to anchor the controller when it is visible.
*/
fun setAnchorView(view: ViewGroup) {
mAnchor = view
val frameParams = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
removeAllViews()
val v = makeControllerView()
addView(v, frameParams)
}
/**
* Create the view that holds the widgets that control playback.
* Derived classes can override this to create their own.
* @return The controller view.
* @hide This doesn't work as advertised
*/
protected fun makeControllerView(): View {
val inflate = mContext!!.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
mRoot = inflate.inflate(R.layout.media_controller, null)
initControllerView(mRoot!!)
return mRoot!!
}
private fun initControllerView(v: View) {
if (mFromXml == null){
return
}
mPauseButton = v.findViewById(R.id.pause) as ImageButton
if (mPauseButton != null) {
mPauseButton!!.requestFocus()
mPauseButton!!.setOnClickListener(mPauseListener)
}
mFullscreenButton = v.findViewById(R.id.fullscreen) as ImageButton
if (mFullscreenButton != null) {
mFullscreenButton!!.requestFocus()
mFullscreenButton!!.setOnClickListener(mFullscreenListener)
}
mProgress = v.findViewById(R.id.mediacontroller_progress) as ProgressBar
if (mProgress != null) {
if (mProgress is SeekBar) {
val seeker = mProgress as SeekBar?
seeker!!.setOnSeekBarChangeListener(mSeekListener)
}
mProgress!!.max = 1000
}
mEndTime = v.findViewById(R.id.time) as TextView
mCurrentTime = v.findViewById(R.id.time_current) as TextView
mFormatBuilder = StringBuilder()
mFormatter = Formatter(mFormatBuilder, Locale.getDefault())
installPrevNextListeners()
}
/**
* Disable pause or seek buttons if the stream cannot be paused or seeked.
* This requires the control interface to be a MediaPlayerControlExt
*/
private fun disableUnsupportedButtons() {
if (mPlayer == null) {
return
}
try {
if (mPauseButton != null && !mPlayer!!.canPause()) {
mPauseButton!!.isEnabled = false
}
if (mRewButton != null && !mPlayer!!.canSeekBackward()) {
mRewButton!!.isEnabled = false
}
if (mFfwdButton != null && !mPlayer!!.canSeekForward()) {
mFfwdButton!!.isEnabled = false
}
} catch (ex: IncompatibleClassChangeError) {
// We were given an old version of the interface, that doesn't have
// the canPause/canSeekXYZ methods. This is OK, it just means we
// assume the media can be paused and seeked, and so we don't disable
// the buttons.
}
}
/**
* Show the controller on screen. It will go away
* automatically after 'timeout' milliseconds of inactivity.
* @param timeout The timeout in milliseconds. Use 0 to show
* the controller until hide() is called.
*/
@JvmOverloads
fun show(timeout: Int = sDefaultTimeout) {
if (!isShowing && mAnchor != null) {
setProgress()
if (mPauseButton != null) {
mPauseButton!!.requestFocus()
}
disableUnsupportedButtons()
val tlp = FrameLayout.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
Gravity.BOTTOM
)
mAnchor!!.addView(this, tlp)
isShowing = true
}
updatePausePlay()
updateFullScreen()
// cause the progress bar to be updated even if mShowing
// was already true. This happens, for example, if we're
// paused with the progress bar showing the user hits play.
mHandler.sendEmptyMessage(SHOW_PROGRESS)
val msg = mHandler.obtainMessage(FADE_OUT)
if (timeout != 0) {
mHandler.removeMessages(FADE_OUT)
mHandler.sendMessageDelayed(msg, timeout.toLong())
}
}
/**
* Remove the controller from the screen.
*/
fun hide() {
if (mAnchor == null) {
return
}
try {
mAnchor!!.removeView(this)
mHandler.removeMessages(SHOW_PROGRESS)
} catch (ex: IllegalArgumentException) {
Log.w("MediaController", "already removed")
}
isShowing = false
}
private fun stringForTime(timeMs: Int): String {
val totalSeconds = timeMs / 1000
val seconds = totalSeconds % 60
val minutes = totalSeconds / 60 % 60
val hours = totalSeconds / 3600
mFormatBuilder?.setLength(0)
return if (hours > 0) {
mFormatter?.format("%d:%02d:%02d", hours, minutes, seconds).toString()
} else {
mFormatter?.format("%02d:%02d", minutes, seconds).toString()
}
}
private fun setProgress(): Int {
if (mPlayer == null || mDragging) {
return 0
}
val position = mPlayer!!.getCurrentPosition()
val duration = mPlayer!!.getDuration()
if (mProgress != null) {
if (duration > 0) {
// use long to avoid overflow
val pos = 1000L * position / duration
mProgress!!.progress = pos.toInt()
}
val percent = mPlayer!!.getBufferPercentage()
mProgress!!.secondaryProgress = percent * 10
}
if (mEndTime != null)
mEndTime!!.text = stringForTime(duration)
if (mCurrentTime != null)
mCurrentTime!!.text = stringForTime(position)
return position
}
override fun onTouchEvent(event: MotionEvent): Boolean {
show(sDefaultTimeout)
return true
}
override fun onTrackballEvent(ev: MotionEvent): Boolean {
show(sDefaultTimeout)
return false
}
override fun dispatchKeyEvent(event: KeyEvent): Boolean {
if (mPlayer == null) {
return true
}
val keyCode = event.keyCode
val uniqueDown = event.repeatCount == 0 && event.action == KeyEvent.ACTION_DOWN
if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK
|| keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|| keyCode == KeyEvent.KEYCODE_SPACE
) {
if (uniqueDown) {
doPauseResume()
show(sDefaultTimeout)
if (mPauseButton != null) {
mPauseButton!!.requestFocus()
}
}
return true
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
if (uniqueDown && !mPlayer!!.isPlaying()) {
mPlayer!!.start()
updatePausePlay()
show(sDefaultTimeout)
}
return true
} else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
if (uniqueDown && mPlayer!!.isPlaying()) {
mPlayer!!.pause()
updatePausePlay()
show(sDefaultTimeout)
}
return true
} else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
|| keyCode == KeyEvent.KEYCODE_VOLUME_UP
|| keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
) {
// don't show the controls for volume adjustment
return super.dispatchKeyEvent(event)
} else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
if (uniqueDown) {
hide()
}
return true
}
show(sDefaultTimeout)
return super.dispatchKeyEvent(event)
}
fun updatePausePlay() {
if (mRoot == null || mPauseButton == null || mPlayer == null) {
return
}
if (mPlayer!!.isPlaying()) {
mPauseButton!!.setImageResource(R.drawable.ic_pause_black_24dp)
} else {
mPauseButton!!.setImageResource(R.drawable.ic_play_arrow_black_24dp)
}
}
fun updateFullScreen() {
if (mRoot == null || mFullscreenButton == null || mPlayer == null) {
return
}
if (mPlayer!!.isFullScreen()) {
mFullscreenButton!!.setImageResource(R.drawable.ic_fullscreen_exit_black_24dp)
} else {
mFullscreenButton!!.setImageResource(R.drawable.ic_fullscreen_black_24dp)
}
}
private fun doPauseResume() {
if (mPlayer == null) {
return
}
if (mPlayer!!.isPlaying()) {
mPlayer!!.pause()
} else {
mPlayer!!.start()
}
updatePausePlay()
}
private fun doToggleFullscreen() {
if (mPlayer == null) {
return
}
mPlayer!!.toggleFullScreen()
}
override fun setEnabled(enabled: Boolean) {
if (mPauseButton != null) {
mPauseButton!!.isEnabled = enabled
}
if (mFfwdButton != null) {
mFfwdButton!!.isEnabled = enabled
}
if (mRewButton != null) {
mRewButton!!.isEnabled = enabled
}
if (mNextButton != null) {
mNextButton!!.isEnabled = enabled && mNextListener != null
}
if (mPrevButton != null) {
mPrevButton!!.isEnabled = enabled && mPrevListener != null
}
if (mProgress != null) {
mProgress!!.isEnabled = enabled
}
disableUnsupportedButtons()
super.setEnabled(enabled)
}
override fun onInitializeAccessibilityEvent(event: AccessibilityEvent) {
super.onInitializeAccessibilityEvent(event)
event.className = MyMediaController::class.java.name
}
override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo) {
super.onInitializeAccessibilityNodeInfo(info)
info.className = MyMediaController::class.java.name
}
private fun installPrevNextListeners() {
if (mNextButton != null) {
mNextButton!!.setOnClickListener(mNextListener)
mNextButton!!.isEnabled = mNextListener != null
}
if (mPrevButton != null) {
mPrevButton!!.setOnClickListener(mPrevListener)
mPrevButton!!.isEnabled = mPrevListener != null
}
}
fun setPrevNextListeners(next: View.OnClickListener, prev: View.OnClickListener) {
mNextListener = next
mPrevListener = prev
mListenersSet = true
if (mRoot != null) {
installPrevNextListeners()
if (mNextButton != null && !mFromXml!!) {
mNextButton!!.visibility = View.VISIBLE
}
if (mPrevButton != null && !mFromXml!!) {
mPrevButton!!.visibility = View.VISIBLE
}
}
}
interface MediaPlayerControl {
fun getDuration() : Int
fun getCurrentPosition() : Int
fun isPlaying() : Boolean
fun getBufferPercentage() : Int
fun isFullScreen() : Boolean
fun start()
fun pause()
fun seekTo(pos: Int)
fun canPause(): Boolean
fun canSeekBackward(): Boolean
fun canSeekForward(): Boolean
fun toggleFullScreen()
}
private class MessageHandler internal constructor(view: MyMediaController) : Handler() {
private val mView: WeakReference<MyMediaController>
init {
mView = WeakReference(view)
}
override fun handleMessage(msg: Message) {
var msg = msg
val view = mView.get()
if (view == null || view.mPlayer == null) {
return
}
val pos: Int
when (msg.what) {
FADE_OUT -> view.hide()
SHOW_PROGRESS -> {
pos = view.setProgress()
if (!view.mDragging && view.isShowing && view.mPlayer!!.isPlaying()) {
msg = obtainMessage(SHOW_PROGRESS)
sendMessageDelayed(msg, (1000 - pos % 1000).toLong())
}
}
}
}
}
companion object {
private val TAG = "VideoControllerView"
private val sDefaultTimeout = 3000
private val FADE_OUT = 1
private val SHOW_PROGRESS = 2
}
}
/**
* Show the controller on screen. It will go away
* automatically after 3 seconds of inactivity.
*/
Save this file as MyMediaController.kt
Step 2.
Add SurfaceView & FrameLayout to Main Activity.
<FrameLayout
android:id="@+id/videoSurfaceContainer"
android:layout_width="match_parent"
android:layout_height="200dp" >
<SurfaceView
android:id="@+id/videoSurface"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</FrameLayout>
Step 3 : Setup MainActivity.kt
First of all implement Some classes to your activity like this.
class MainActivity : AppCompatActivity(), SurfaceHolder.Callback, MediaPlayer.OnPreparedListener, MyMediaController.MediaPlayerControl{
Not include the function by pressing ALT + Enter.
Here is the code for functions included functions.
override fun surfaceChanged(holder: SurfaceHolder?, format: Int, width: Int, height: Int) {
}
override fun surfaceDestroyed(holder: SurfaceHolder?) {
}
override fun surfaceCreated(holder: SurfaceHolder?) {
player?.setDisplay(holder);
player?.prepareAsync();
}
override fun onPrepared(mp: MediaPlayer?) {
controller?.setMediaPlayer(this);
controller?.setAnchorView(findViewById<FrameLayout>(R.id.videoSurfaceContainer));
player?.start();
}
override fun start() {
if (player!=null) {
return player?.start()!!;
}
}
override fun pause() {
if (player!=null) {
return player?.pause()!!;
}
}
override fun getDuration(): Int {
if (player!=null) {
return player?.duration!!;
}else{
return 0
}
}
override fun getCurrentPosition(): Int {
if (player!=null) {
return player?.getCurrentPosition()!!;
}else{
return 0
}
}
override fun seekTo(pos: Int) {
if (player!=null) {
return player?.seekTo(pos)!!;
}
}
override fun isPlaying(): Boolean {
if (player!=null) {
return player?.isPlaying()!!;
}else{
return false
}
}
override fun getBufferPercentage(): Int {
return 0;
}
override fun canPause(): Boolean {
return true
}
override fun canSeekBackward(): Boolean {
return true
}
override fun canSeekForward(): Boolean {
return true
}
override fun isFullScreen(): Boolean {
if (requestedOrientation == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE){
return true
}else {
return false
}
}
override fun onConfigurationChanged(newConfig: Configuration?) {
super.onConfigurationChanged(newConfig)
if(newConfig?.orientation == Configuration.ORIENTATION_PORTRAIT){
Toast.makeText(applicationContext,"Portrait ",Toast.LENGTH_SHORT).show()
}
if(newConfig?.orientation == Configuration.ORIENTATION_LANDSCAPE){
Toast.makeText(applicationContext,"Landscape ",Toast.LENGTH_SHORT).show()
}
}
override fun toggleFullScreen() {
if (isFullScreen){
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}else{
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
Toast.makeText(applicationContext,"Toggle Full",Toast.LENGTH_SHORT).show()
//controller?.updateFullScreen()
}
Now the main work connecting our surfaceView to our MediaController
val videoHolder = videoSurface.holder
videoHolder.addCallback(this)
player = MediaPlayer()
controller = MyMediaController(this)
val videoController = MediaController(this)
try {
player?.setAudioStreamType(AudioManager.STREAM_MUSIC)
player?.setDataSource(this, Uri.parse("http://clips.vorwaerts-gmbh.de/big_buck_bunny.mp4"))
player?.setOnPreparedListener(this)
} catch (e: IllegalArgumentException) {
e.printStackTrace()
} catch (e: SecurityException) {
e.printStackTrace()
} catch (e: IllegalStateException) {
e.printStackTrace()
} catch (e: IOException) {
e.printStackTrace()
}
Add onTouchEvent to show the controller on touch.
override fun onTouchEvent(event: MotionEvent): Boolean {
controller?.show()
return false
}
Here we go, our VideoView is ready to fly.
If you have any confusion in MainActivity Code refer here to Check full activity code.