ExoPlayer was developed by Google for media-centric applications like YouTube and then it was turned on to the community. It is successful, fast, and stable. So we will be using ExoPlayer for playing video in our tutorial.
Create a new project. Make sure you check the option for Kotlin support while creating the project. If you have an existing project without Kotlin support, you can still add it, just follow the tutorial ahead and add the missing fields in your Gradle files.
Your project level build.gradle file should look like this:
buildscript { ext.kotlin_version = '1.2.71' repositories { google() jcenter() maven { url "https://jitpack.io" } } dependencies { classpath 'com.android.tools.build:gradle:3.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } allprojects { repositories { google() jcenter() maven { url "https://jitpack.io" } } } task clean(type: Delete) { delete rootProject.buildDir }
and your app level build.gradle file
apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 28 defaultConfig { applicationId "com.your.package" minSdkVersion 16 targetSdkVersion 28 versionCode 4 versionName "1.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) //for kotlin implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" //other dependencies implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.android.support:design:28.0.0' implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation 'com.android.support:support-vector-drawable:28.0.0' implementation 'com.android.support:recyclerview-v7:28.0.0' implementation 'com.jakewharton:butterknife:8.8.1' implementation 'com.android.support:multidex:1.0.3' //exoplayer implementation 'com.google.android.exoplayer:exoplayer:2.9.1' implementation 'com.google.android.exoplayer:extension-mediasession:2.8.1' 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' }
Create a layout for your video player
act_video.xml
<?xml version="1.0" encoding="utf-8"?> <android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:background="@android:color/black"> <com.google.android.exoplayer2.ui.PlayerView android:id="@+id/playerView" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_gravity="center" /> </android.support.constraint.ConstraintLayout>
Create new Kotlin class for activity
VideoActivity.kt
import android.app.Activity import android.app.PictureInPictureParams import android.content.pm.PackageManager import android.content.res.Configuration import android.net.Uri import android.os.Build import android.os.Bundle import android.os.Handler import android.support.annotation.RequiresApi import android.support.v4.media.session.MediaSessionCompat import android.support.v7.app.AppCompatActivity import com.google.android.exoplayer2.* import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.source.ExtractorMediaSource import com.google.android.exoplayer2.source.TrackGroupArray import com.google.android.exoplayer2.source.hls.HlsMediaSource import com.google.android.exoplayer2.trackselection.DefaultTrackSelector import com.google.android.exoplayer2.trackselection.TrackSelectionArray import com.google.android.exoplayer2.ui.PlayerView import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory import com.google.android.exoplayer2.util.Util class VideoActivity: AppCompatActivity() { companion object { @JvmField val ARG_VIDEO_URL = "VideoActivity.URL" val ARG_VIDEO_POSITION = "VideoActivity.POSITION" } var isInPipMode:Boolean = false lateinit var mUrl: String lateinit var player : SimpleExoPlayer private var videoPosition:Long = 0L var isPIPModeeEnabled:Boolean = true //Has the user disabled PIP mode in AppOpps? lateinit var playerView : PlayerView override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.act_video) playerView = findViewById(R.id.playerView); if (intent.extras == null || !intent.hasExtra(ARG_VIDEO_URL)) { finish() } mUrl = intent.getStringExtra(ARG_VIDEO_URL) savedInstanceState?.let { videoPosition = savedInstanceState.getLong(ARG_VIDEO_POSITION) } } override fun onStart() { super.onStart() player = ExoPlayerFactory.newSimpleInstance(this, DefaultTrackSelector()) playerView.player = player val dataSourceFactory = DefaultDataSourceFactory(this, Util.getUserAgent(this, applicationInfo.loadLabel(packageManager).toString())) when (Util.inferContentType(Uri.parse(mUrl))) { C.TYPE_HLS -> { val mediaSource = HlsMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(mUrl)) player.prepare(mediaSource) } C.TYPE_OTHER -> { val mediaSource = ExtractorMediaSource.Factory(dataSourceFactory).createMediaSource(Uri.parse(mUrl)) player.prepare(mediaSource) } else -> { //This is to catch SmoothStreaming and DASH types which are not supported currently setResult(Activity.RESULT_CANCELED) finish() } } var returnResultOnce:Boolean = true player.addListener(object : Player.EventListener{ override fun onTimelineChanged(timeline: Timeline?, manifest: Any?, reason: Int) {} override fun onShuffleModeEnabledChanged(shuffleModeEnabled: Boolean) {} override fun onRepeatModeChanged(repeatMode: Int) {} override fun onPositionDiscontinuity(reason: Int) {} override fun onLoadingChanged(isLoading: Boolean) {} override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {} override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters?) {} override fun onPlayerError(error: ExoPlaybackException?) { setResult(Activity.RESULT_CANCELED) //Use finish() if minSdkVersion is <21. else use finishAndRemoveTask() finish() //finishAndRemoveTask() } override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) { if(playbackState == Player.STATE_READY && returnResultOnce){ setResult(Activity.RESULT_OK) returnResultOnce = false } } override fun onSeekProcessed() {} }) player.playWhenReady = true //Use Media Session Connector from the EXT library to enable MediaSession Controls in PIP. val mediaSession = MediaSessionCompat(this, packageName) mediaSession.setFlags(MediaSessionCompat.FLAG_HANDLES_MEDIA_BUTTONS or MediaSessionCompat.FLAG_HANDLES_TRANSPORT_CONTROLS) val mediaSessionConnector = MediaSessionConnector(mediaSession) mediaSessionConnector.setPlayer(player, null) mediaSession.isActive = true } override fun onPause() { videoPosition = player.currentPosition super.onPause() } override fun onResume() { super.onResume() if(videoPosition > 0L && !isInPipMode){ player.seekTo(videoPosition) } //Makes sure that the media controls pop up on resuming and when going between PIP and non-PIP states. playerView.useController = true } override fun onStop() { super.onStop() playerView.player = null player.release() //PIPmode activity.finish() does not remove the activity from the recents stack. //Only finishAndRemoveTask does this. //But here we are using finish() because our Mininum SDK version is 16 //If you use minSdkVersion as 21+ then remove finish() and use finishAndRemoveTask() instead if ((Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { finish() //finishAndRemoveTask() } } override fun onSaveInstanceState(outState: Bundle?) { super.onSaveInstanceState(outState.apply { this?.putLong(ARG_VIDEO_POSITION, player.currentPosition) }) } override fun onRestoreInstanceState(savedInstanceState: Bundle?) { super.onRestoreInstanceState(savedInstanceState) videoPosition = savedInstanceState!!.getLong(ARG_VIDEO_POSITION) } override fun onBackPressed(){ if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE) && isPIPModeeEnabled) { enterPIPMode() } else { super.onBackPressed() } } override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { if(newConfig !=null){ videoPosition = player.currentPosition isInPipMode = !isInPictureInPictureMode } super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) } //Called when the user touches the Home or Recents button to leave the app. override fun onUserLeaveHint() { super.onUserLeaveHint() enterPIPMode() } @Suppress("DEPRECATION") fun enterPIPMode(){ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) { videoPosition = player.currentPosition playerView.useController = false if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { val params = PictureInPictureParams.Builder() this.enterPictureInPictureMode(params.build()) } else { this.enterPictureInPictureMode() } /* We need to check this because the system permission check is publically hidden for integers for non-manufacturer-built apps https://github.com/aosp-mirror/platform_frameworks_base/blob/studio-3.1.2/core/java/android/app/AppOpsManager.java#L1640 ********* If we didn't have that problem ********* val appOpsManager = getSystemService(Context.APP_OPS_SERVICE) as AppOpsManager if(appOpsManager.checkOpNoThrow(AppOpManager.OP_PICTURE_IN_PICTURE, packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA).uid, packageName) == AppOpsManager.MODE_ALLOWED) 30MS window in even a restricted memory device (756mb+) is more than enough time to check, but also not have the system complain about holding an action hostage. */ Handler().postDelayed({checkPIPPermission()}, 30) } } @RequiresApi(Build.VERSION_CODES.N) fun checkPIPPermission(){ isPIPModeeEnabled = isInPictureInPictureMode if(!isInPictureInPictureMode){ onBackPressed() } } }
In AndroidManifest.xml make sure to add Internet permission
<uses-permission android:name="android.permission.INTERNET" />
and declare that your activity supports Picture-in-Picture
<activity android:name=".VideoActivity" android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" android:resizeableActivity="true" android:launchMode="singleTask" android:supportsPictureInPicture="true" android:exported="false" tools:targetApi="n" />
The attribute of launch Mode singleTask tells Android that there should be only one instance of that Activity.
Now from your main activity, launch this activity wherever you like, passing along the URL.
Intent intent = new Intent(MainActivity.this, VideoActivity.class); intent.putExtra("VideoActivity.URL","https://www.youtube.com/watch?v=C0DPdy98e4c"); startActivity(intent);
—
Leave A Comment