migrated to retrofit+jackson

This commit is contained in:
rimthekid 2021-12-18 15:04:14 +04:00
parent 9a5bb5aac8
commit c356f9b426
21 changed files with 517 additions and 307 deletions

View File

@ -1,17 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown>
<Target>
<type value="RUNNING_DEVICE_TARGET" />
<deviceKey>
<Key>
<type value="VIRTUAL_DEVICE_PATH" />
<value value="$USER_HOME$/.android/avd/Pixel_5_API_29.avd" />
</Key>
</deviceKey>
</Target>
</runningDeviceTargetSelectedWithDropDown>
<targetSelectedWithDropDown> <targetSelectedWithDropDown>
<Target> <Target>
<type value="QUICK_BOOT_TARGET" /> <type value="QUICK_BOOT_TARGET" />
@ -23,7 +12,7 @@
</deviceKey> </deviceKey>
</Target> </Target>
</targetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2021-12-17T12:12:37.849691Z" /> <timeTargetWasSelectedWithDropDown value="2021-12-17T12:50:01.440166Z" />
<runningDeviceTargetsSelectedWithDialog> <runningDeviceTargetsSelectedWithDialog>
<Target> <Target>
<type value="RUNNING_DEVICE_TARGET" /> <type value="RUNNING_DEVICE_TARGET" />

View File

@ -8,7 +8,7 @@ android {
defaultConfig { defaultConfig {
applicationId 'xyz.btcland.libretube' applicationId 'xyz.btcland.libretube'
minSdk 19 minSdk 21
targetSdk 31 targetSdk 31
versionCode 1 versionCode 1
versionName '1.0' versionName '1.0'
@ -39,14 +39,16 @@ dependencies {
implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01' implementation 'androidx.navigation:navigation-fragment-ktx:2.4.0-rc01'
implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01' implementation 'androidx.navigation:navigation-ui-ktx:2.4.0-rc01'
implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.legacy:legacy-support-v4:1.0.0'
testImplementation 'junit:junit:'
androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.ext:junit:1.1.3'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'com.squareup.picasso:picasso:2.8' implementation 'com.squareup.picasso:picasso:2.8'
implementation 'de.hdodenhof:circleimageview:3.1.0' implementation 'de.hdodenhof:circleimageview:3.1.0'
implementation 'com.google.android.exoplayer:exoplayer:2.16.1' implementation 'com.google.android.exoplayer:exoplayer:2.16.1'
implementation 'androidx.multidex:multidex:2.0.1' implementation 'androidx.multidex:multidex:2.0.1'
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-jackson:2.9.0'
} }

View File

@ -1,24 +1,27 @@
package xyz.btcland.libretube package xyz.btcland.libretube
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ProgressBar import android.widget.ProgressBar
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import okhttp3.* import okhttp3.*
import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import java.lang.Exception
// TODO: Rename parameter arguments, choose names that match // TODO: Rename parameter arguments, choose names that match
// the fragment initialization parameters, e.g. ARG_ITEM_NUMBER // the fragment initialization parameters, e.g. ARG_ITEM_NUMBER
private const val ARG_PARAM1 = "param1" private const val ARG_PARAM1 = "param1"
private const val ARG_PARAM2 = "param2" private const val ARG_PARAM2 = "param2"
const val TAG = "HomeFragment"
/** /**
* A simple [Fragment] subclass. * A simple [Fragment] subclass.
* Use the [Home.newInstance] factory method to * Use the [Home.newInstance] factory method to
@ -79,10 +82,10 @@ class Home : Fragment() {
} }
private fun fetchJson(progressBar: ProgressBar, recyclerView: RecyclerView) { private fun fetchJson(progressBar: ProgressBar, recyclerView: RecyclerView) {
val client = OkHttpClient() //val client = OkHttpClient()
fun run() { fun run() {
val request = Request.Builder() /* val request = Request.Builder()
.url("http://piped-api.alefvanoon.xyz/trending?region=US") .url("http://piped-api.alefvanoon.xyz/trending?region=US")
.build() .build()
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
@ -102,10 +105,24 @@ class Home : Fragment() {
recyclerView.adapter = TrendingAdapter(trendingList) recyclerView.adapter = TrendingAdapter(trendingList)
} }
} }
} }
}) })*/
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getTrending("US")
}catch(e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
runOnUiThread {
progressBar.visibility = View.GONE
recyclerView.adapter = TrendingAdapter(response)
}
}
} }
run() run()

View File

@ -0,0 +1,15 @@
package xyz.btcland.libretube
import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.http.Query
import xyz.btcland.libretube.obj.StreamItem
import xyz.btcland.libretube.obj.Streams
interface PipedApi {
@GET("trending")
suspend fun getTrending(@Query("region") region: String): List<StreamItem>
@GET("streams/{videoId}")
suspend fun getStreams(@Path("videoId") videoId: String): Streams
}

View File

@ -6,216 +6,20 @@ import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle import android.os.Bundle
import android.view.View
import android.widget.ImageButton
import android.widget.TextView
import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri
import com.google.android.exoplayer2.ExoPlayer
import com.google.android.exoplayer2.MediaItem
import com.google.android.exoplayer2.source.DefaultMediaSourceFactory
import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.source.MergingMediaSource
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultHttpDataSource
import com.google.gson.GsonBuilder
import okhttp3.*
import java.io.IOException
class Player : Activity() { class Player : Activity() {
private lateinit var exoPlayerView: StyledPlayerView
private lateinit var motionLayout: SingleViewTouchableMotionLayout
private lateinit var exoPlayer: ExoPlayer
private var videoId: String? =null
private var seekTo: Long? = 0
private var whichQuality: Int? = 0
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_player) setContentView(R.layout.activity_player)
seekTo = intent.getStringExtra("seekTo")?.toLong()
whichQuality = intent.getStringExtra("quality")?.toInt()
videoId=intent.getStringExtra("videoId")
exoPlayerView = findViewById(R.id.fullscreen_player)
fetchJson(this)
}
private fun fetchJson(context: Context) {
val client = OkHttpClient()
fun run() {
val request = Request.Builder()
.url("https://pipedapi.kavin.rocks/streams/$videoId")
.build()
client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
e.printStackTrace()
}
override fun onResponse(call: Call, response: Response) {
response.use {
if (!response.isSuccessful) throw IOException("Unexpected code $response")
val body = response.body!!.string()
println(body)
val gson = GsonBuilder().create()
val videoInPlayer = gson.fromJson(body, VideoInPlayer::class.java)
var videosNameArray: Array<CharSequence> = arrayOf()
videosNameArray += "HLS"
for (vids in videoInPlayer.videoStreams){
val name = vids.quality +" "+ vids.format
videosNameArray += name
}
runOnUiThread {
exoPlayer = ExoPlayer.Builder(context)
.build()
var subtitle = mutableListOf<MediaItem.SubtitleConfiguration>()
if(videoInPlayer.subtitles.isNotEmpty()){
subtitle?.add(
MediaItem.SubtitleConfiguration.Builder(videoInPlayer.subtitles[0].url.toUri())
.setMimeType(videoInPlayer.subtitles[0].mimeType) // The correct MIME type (required).
.setLanguage(videoInPlayer.subtitles[0].code) // The subtitle language (optional).
.build())}
if(whichQuality==0){
val mediaItem: MediaItem = MediaItem.Builder()
.setUri(videoInPlayer.hls)
.setSubtitleConfigurations(subtitle)
.build()
exoPlayer.setMediaItem(mediaItem)
}else{
val dataSourceFactory: DataSource.Factory =
DefaultHttpDataSource.Factory()
val videoItem: MediaItem = MediaItem.Builder()
.setUri(videoInPlayer.videoStreams[whichQuality!!-1].url)
.setSubtitleConfigurations(subtitle)
.build()
val videoSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(videoItem)
var audioSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(
MediaItem.fromUri(
videoInPlayer.audioStreams[0].url
)
)
if (videoInPlayer.videoStreams[whichQuality!!-1].quality=="720p" || videoInPlayer.videoStreams[whichQuality!!-1].quality=="1080p" || videoInPlayer.videoStreams[whichQuality!!-1].quality=="480p" ){
audioSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(
MediaItem.fromUri(
videoInPlayer.audioStreams[getMostBitRate(
videoInPlayer.audioStreams
)].url
)
)
//println("fuckkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkitttttttttttttttttttttt")
}
val mergeSource: MediaSource = MergingMediaSource(videoSource,audioSource)
exoPlayer.setMediaSource(mergeSource)
}
findViewById<TextView>(R.id.quality_text).text=videosNameArray[whichQuality!!]
exoPlayerView.setShowSubtitleButton(true)
exoPlayerView.setShowNextButton(false)
exoPlayerView.setShowPreviousButton(false)
exoPlayerView.controllerShowTimeoutMs = 1500
exoPlayerView.controllerHideOnTouch = true
exoPlayerView.player = exoPlayer
///exoPlayer.getMediaItemAt(5)
exoPlayer.prepare()
exoPlayer.play()
exoPlayer.seekTo(seekTo!!)
findViewById<ImageButton>(R.id.quality_select).setOnClickListener{
val builder: AlertDialog.Builder? = let {
AlertDialog.Builder(context)
}
builder!!.setTitle(R.string.choose_quality_dialog)
.setItems(videosNameArray,
DialogInterface.OnClickListener { _, which ->
// The 'which' argument contains the index position
// of the selected item
//println(which)
if(videoInPlayer.subtitles.isNotEmpty()) {
var subtitle =
mutableListOf<MediaItem.SubtitleConfiguration>()
subtitle?.add(
MediaItem.SubtitleConfiguration.Builder(videoInPlayer.subtitles[0].url.toUri())
.setMimeType(videoInPlayer.subtitles[0].mimeType) // The correct MIME type (required).
.setLanguage(videoInPlayer.subtitles[0].code) // The subtitle language (optional).
.build()
)
}
if(which==0){
val mediaItem: MediaItem = MediaItem.Builder()
.setUri(videoInPlayer.hls)
.setSubtitleConfigurations(subtitle)
.build()
exoPlayer.setMediaItem(mediaItem)
}else{
val dataSourceFactory: DataSource.Factory =
DefaultHttpDataSource.Factory()
val videoItem: MediaItem = MediaItem.Builder()
.setUri(videoInPlayer.videoStreams[which-1].url)
.setSubtitleConfigurations(subtitle)
.build()
val videoSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(videoItem)
var audioSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(
MediaItem.fromUri(
videoInPlayer.audioStreams[0].url
)
)
if (videoInPlayer.videoStreams[which-1].quality=="720p" || videoInPlayer.videoStreams[which-1].quality=="1080p" || videoInPlayer.videoStreams[which-1].quality=="480p" ){
audioSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(
MediaItem.fromUri(
videoInPlayer.audioStreams[getMostBitRate(
videoInPlayer.audioStreams
)].url
)
)
//println("fuckkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkitttttttttttttttttttttt")
}
val mergeSource: MediaSource = MergingMediaSource(videoSource,audioSource)
exoPlayer.setMediaSource(mergeSource)
}
findViewById<TextView>(R.id.quality_text).text=videosNameArray[which]
})
val dialog: AlertDialog? = builder?.create()
dialog?.show()
}
}
}
}
})
}
run()
} }
fun getMostBitRate(audios: List<Stream>):Int{
var bitrate =0
var index = 0
for ((i, audio) in audios.withIndex()){
val q = audio.quality.replace(" kbps","").toInt()
if (q>bitrate){
bitrate=q
index = i
}
}
return index
}
override fun onStop() {
super.onStop()
myApp.seekTo=exoPlayer.currentPosition
exoPlayer.stop()
}
} }

View File

@ -18,8 +18,7 @@ import com.google.android.exoplayer2.source.MediaSource
import com.google.android.exoplayer2.ui.PlayerView import com.google.android.exoplayer2.ui.PlayerView
import com.google.android.exoplayer2.ui.StyledPlayerControlView import com.google.android.exoplayer2.ui.StyledPlayerControlView
import com.google.android.exoplayer2.ui.StyledPlayerView import com.google.android.exoplayer2.ui.StyledPlayerView
import com.google.gson.GsonBuilder
import com.google.gson.reflect.TypeToken
import okhttp3.* import okhttp3.*
import java.io.IOException import java.io.IOException
import kotlin.math.abs import kotlin.math.abs
@ -47,9 +46,11 @@ import android.widget.TextView
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.google.android.exoplayer2.util.Util import com.google.android.exoplayer2.util.Util
import android.graphics.drawable.ColorDrawable import android.graphics.drawable.ColorDrawable
import android.util.Log
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.text.PrecomputedTextCompat import androidx.core.text.PrecomputedTextCompat
import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.exoplayer2.Player import com.google.android.exoplayer2.Player
@ -59,6 +60,15 @@ import com.google.android.exoplayer2.util.RepeatModeUtil
import com.google.android.exoplayer2.ui.TimeBar import com.google.android.exoplayer2.ui.TimeBar
import com.google.android.exoplayer2.ui.TimeBar.OnScrubListener import com.google.android.exoplayer2.ui.TimeBar.OnScrubListener
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import retrofit2.HttpException
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.GET
import retrofit2.http.Path
import xyz.btcland.libretube.obj.PipedStream
// TODO: Rename parameter arguments, choose names that match // TODO: Rename parameter arguments, choose names that match
@ -186,12 +196,13 @@ class PlayerFragment : Fragment() {
true true
} }
} }
//FullScreen button trigger
view.findViewById<ImageButton>(R.id.fullscreen).setOnClickListener{ view.findViewById<ImageButton>(R.id.fullscreen).setOnClickListener{
//remember to hide everything when new shit added //remember to hide everything when new shit added
if (!isFullScreen){ if (!isFullScreen){
view.findViewById<ScrollView>(R.id.scrollView2).visibility = View.GONE /*view.findViewById<ScrollView>(R.id.scrollView2).visibility = View.GONE
view.findViewById<LinearLayout>(R.id.linLayout).visibility = View.GONE view.findViewById<LinearLayout>(R.id.linLayout).visibility = View.GONE
view.findViewById<TextView>(R.id.textTest).visibility = View.GONE view.findViewById<TextView>(R.id.textTest).visibility = View.GONE*/
//view.findViewById<ConstraintLayout>(R.id.main_container).visibility = View.GONE //view.findViewById<ConstraintLayout>(R.id.main_container).visibility = View.GONE
with(motionLayout) { with(motionLayout) {
getConstraintSet(R.id.start).constrainHeight(R.id.player, -1) getConstraintSet(R.id.start).constrainHeight(R.id.player, -1)
@ -200,9 +211,9 @@ class PlayerFragment : Fragment() {
isFullScreen=true isFullScreen=true
}else{ }else{
view.findViewById<ScrollView>(R.id.scrollView2).visibility = View.VISIBLE /*view.findViewById<ScrollView>(R.id.scrollView2).visibility = View.VISIBLE
view.findViewById<LinearLayout>(R.id.linLayout).visibility = View.VISIBLE view.findViewById<LinearLayout>(R.id.linLayout).visibility = View.VISIBLE
view.findViewById<TextView>(R.id.textTest).visibility = View.VISIBLE view.findViewById<TextView>(R.id.textTest).visibility = View.VISIBLE*/
//view.findViewById<ConstraintLayout>(R.id.main_container).visibility = View.VISIBLE //view.findViewById<ConstraintLayout>(R.id.main_container).visibility = View.VISIBLE
with(motionLayout) { with(motionLayout) {
getConstraintSet(R.id.start).constrainHeight(R.id.player, 0) getConstraintSet(R.id.start).constrainHeight(R.id.player, 0)
@ -245,12 +256,19 @@ class PlayerFragment : Fragment() {
} }
private fun fetchJson(view: View) { private fun fetchJson(view: View) {
val client = OkHttpClient() //val client = OkHttpClient()
fun run() { fun run() {
val request = Request.Builder() /* val request = Request.Builder()
.url("http://piped-api.alefvanoon.xyz/streams/$videoId") .url("http://piped-api.alefvanoon.xyz/streams/$videoId")
.build() .build()
*//* val retrofit = Retrofit.Builder()
.baseUrl("http://piped-api.alefvanoon.xyz/")
.addConverterFactory(JacksonConverterFactory.create())
.build()
val videoInPlayer2 = retrofit.create(vidVid::class.java).vidIn(videoId)*//*
client.newCall(request).enqueue(object : Callback { client.newCall(request).enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) { override fun onFailure(call: Call, e: IOException) {
e.printStackTrace() e.printStackTrace()
@ -264,8 +282,8 @@ class PlayerFragment : Fragment() {
val videoInPlayer = gson.fromJson(body, VideoInPlayer::class.java) val videoInPlayer = gson.fromJson(body, VideoInPlayer::class.java)
var videosNameArray: Array<CharSequence> = arrayOf() var videosNameArray: Array<CharSequence> = arrayOf()
videosNameArray += "HLS" videosNameArray += "HLS"
for (vids in videoInPlayer.videoStreams){ for (vid in videoInPlayer.videoStreams){
val name = vids.quality +" "+ vids.format val name = vid.quality +" "+ vid.format
videosNameArray += name videosNameArray += name
} }
runOnUiThread { runOnUiThread {
@ -295,15 +313,13 @@ class PlayerFragment : Fragment() {
view.findViewById<TextView>(R.id.title_textView).text = videoInPlayer.title view.findViewById<TextView>(R.id.title_textView).text = videoInPlayer.title
view.findViewById<ImageButton>(R.id.quality_select).setOnClickListener{ view.findViewById<ImageButton>(R.id.quality_select).setOnClickListener{
//Dialog for quality selection
val builder: AlertDialog.Builder? = activity?.let { val builder: AlertDialog.Builder? = activity?.let {
AlertDialog.Builder(it) AlertDialog.Builder(it)
} }
builder!!.setTitle(R.string.choose_quality_dialog) builder!!.setTitle(R.string.choose_quality_dialog)
.setItems(videosNameArray, .setItems(videosNameArray,
DialogInterface.OnClickListener { _, which -> DialogInterface.OnClickListener { _, which ->
// The 'which' argument contains the index position
// of the selected item
//println(which)
whichQuality = which whichQuality = which
if(videoInPlayer.subtitles.isNotEmpty()) { if(videoInPlayer.subtitles.isNotEmpty()) {
var subtitle = var subtitle =
@ -345,6 +361,7 @@ class PlayerFragment : Fragment() {
val dialog: AlertDialog? = builder?.create() val dialog: AlertDialog? = builder?.create()
dialog?.show() dialog?.show()
} }
//Listener for play and pause icon change
exoPlayer!!.addListener(object : com.google.android.exoplayer2.Player.Listener { exoPlayer!!.addListener(object : com.google.android.exoplayer2.Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean,playbackState: Int) { override fun onPlayerStateChanged(playWhenReady: Boolean,playbackState: Int) {
if (playWhenReady && playbackState == Player.STATE_READY) { if (playWhenReady && playbackState == Player.STATE_READY) {
@ -366,22 +383,134 @@ class PlayerFragment : Fragment() {
} }
}) })*/
lifecycleScope.launchWhenCreated {
val response = try {
RetrofitInstance.api.getStreams(videoId!!)
}catch(e: IOException) {
println(e)
Log.e(TAG, "IOException, you might not have internet connection")
return@launchWhenCreated
} catch (e: HttpException) {
Log.e(TAG, "HttpException, unexpected response")
return@launchWhenCreated
}
var videosNameArray: Array<CharSequence> = arrayOf()
videosNameArray += "HLS"
for (vid in response.videoStreams!!){
val name = vid.quality +" "+ vid.format
videosNameArray += name
}
runOnUiThread {
var subtitle = mutableListOf<SubtitleConfiguration>()
if(response.subtitles!!.isNotEmpty()){
subtitle?.add(SubtitleConfiguration.Builder(response.subtitles!![0].url!!.toUri())
.setMimeType(response.subtitles!![0].mimeType!!) // The correct MIME type (required).
.setLanguage(response.subtitles!![0].code) // The subtitle language (optional).
.build())}
val mediaItem: MediaItem = MediaItem.Builder()
.setUri(response.hls)
.setSubtitleConfigurations(subtitle)
.build()
exoPlayer = ExoPlayer.Builder(view.context)
.build()
exoPlayerView.setShowSubtitleButton(true)
exoPlayerView.setShowNextButton(false)
exoPlayerView.setShowPreviousButton(false)
//exoPlayerView.controllerShowTimeoutMs = 1500
exoPlayerView.controllerHideOnTouch = true
exoPlayerView.player = exoPlayer
exoPlayer.setMediaItem(mediaItem)
///exoPlayer.getMediaItemAt(5)
exoPlayer.prepare()
exoPlayer.play()
view.findViewById<TextView>(R.id.title_textView).text = response.title
view.findViewById<ImageButton>(R.id.quality_select).setOnClickListener{
//Dialog for quality selection
val builder: AlertDialog.Builder? = activity?.let {
AlertDialog.Builder(it)
}
builder!!.setTitle(R.string.choose_quality_dialog)
.setItems(videosNameArray,
DialogInterface.OnClickListener { _, which ->
whichQuality = which
if(response.subtitles!!.isNotEmpty()) {
var subtitle =
mutableListOf<SubtitleConfiguration>()
subtitle?.add(
SubtitleConfiguration.Builder(response.subtitles!![0].url!!.toUri())
.setMimeType(response.subtitles!![0].mimeType!!) // The correct MIME type (required).
.setLanguage(response.subtitles!![0].code) // The subtitle language (optional).
.build()
)
}
if(which==0){
val mediaItem: MediaItem = MediaItem.Builder()
.setUri(response.hls)
.setSubtitleConfigurations(subtitle)
.build()
exoPlayer.setMediaItem(mediaItem)
}else{
val dataSourceFactory: DataSource.Factory =
DefaultHttpDataSource.Factory()
val videoItem: MediaItem = MediaItem.Builder()
.setUri(response.videoStreams[which-1].url)
.setSubtitleConfigurations(subtitle)
.build()
val videoSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(videoItem)
var audioSource: MediaSource = DefaultMediaSourceFactory(dataSourceFactory)
.createMediaSource(fromUri(response.audioStreams!![0].url!!))
if (response.videoStreams[which-1].quality=="720p" || response.videoStreams[which-1].quality=="1080p" || response.videoStreams[which-1].quality=="480p" ){
audioSource = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(fromUri(response.audioStreams!![getMostBitRate(response.audioStreams)].url!!))
}
val mergeSource: MediaSource = MergingMediaSource(videoSource,audioSource)
exoPlayer.setMediaSource(mergeSource)
}
view.findViewById<TextView>(R.id.quality_text).text=videosNameArray[which]
})
val dialog: AlertDialog? = builder?.create()
dialog?.show()
}
//Listener for play and pause icon change
exoPlayer!!.addListener(object : com.google.android.exoplayer2.Player.Listener {
override fun onPlayerStateChanged(playWhenReady: Boolean,playbackState: Int) {
if (playWhenReady && playbackState == Player.STATE_READY) {
// media actually playing
view.findViewById<ImageView>(R.id.play_imageView).setImageResource(R.drawable.ic_pause)
} else if (playWhenReady) {
// might be idle (plays after prepare()),
// buffering (plays when data available)
// or ended (plays when seek away from end)
view.findViewById<ImageView>(R.id.play_imageView).setImageResource(R.drawable.ic_play)
} else {
// player paused in any state
view.findViewById<ImageView>(R.id.play_imageView).setImageResource(R.drawable.ic_play)
}
}
})
}
}
} }
run() run()
} }
fun Fragment?.runOnUiThread(action: () -> Unit) { private fun Fragment?.runOnUiThread(action: () -> Unit) {
this ?: return this ?: return
if (!isAdded) return // Fragment not attached to an Activity if (!isAdded) return // Fragment not attached to an Activity
activity?.runOnUiThread(action) activity?.runOnUiThread(action)
} }
fun getMostBitRate(audios: List<Stream>):Int{ fun getMostBitRate(audios: List<PipedStream>):Int{
var bitrate =0 var bitrate =0
var index = 0 var index = 0
for ((i, audio) in audios.withIndex()){ for ((i, audio) in audios.withIndex()){
val q = audio.quality.replace(" kbps","").toInt() val q = audio.quality!!.replace(" kbps","").toInt()
if (q>bitrate){ if (q>bitrate){
bitrate=q bitrate=q
index = i index = i

View File

@ -0,0 +1,14 @@
package xyz.btcland.libretube
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
object RetrofitInstance {
val api: PipedApi by lazy {
Retrofit.Builder()
.baseUrl("https://piped-api.alefvanoon.xyz/")
.addConverterFactory(JacksonConverterFactory.create())
.build()
.create(PipedApi::class.java)
}
}

View File

@ -10,10 +10,11 @@ import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.squareup.picasso.Picasso import com.squareup.picasso.Picasso
import xyz.btcland.libretube.obj.StreamItem
import java.math.BigDecimal import java.math.BigDecimal
import java.math.RoundingMode import java.math.RoundingMode
class TrendingAdapter(private val videoFeed: List<Video>): RecyclerView.Adapter<CustomViewHolder>() { class TrendingAdapter(private val videoFeed: List<StreamItem>): RecyclerView.Adapter<CustomViewHolder>() {
override fun getItemCount(): Int { override fun getItemCount(): Int {
return videoFeed.size return videoFeed.size
} }
@ -40,7 +41,7 @@ class TrendingAdapter(private val videoFeed: List<Video>): RecyclerView.Adapter<
//intent.putExtra("videoId",trending.url.replace("/watch?v=","")) //intent.putExtra("videoId",trending.url.replace("/watch?v=",""))
//holder.v.context.startActivity(intent) //holder.v.context.startActivity(intent)
var bundle = Bundle() var bundle = Bundle()
bundle.putString("videoId",trending.url.replace("/watch?v=","")) bundle.putString("videoId",trending.url!!.replace("/watch?v=",""))
var frag = PlayerFragment() var frag = PlayerFragment()
frag.arguments = bundle frag.arguments = bundle
val activity = holder.v.context as AppCompatActivity val activity = holder.v.context as AppCompatActivity
@ -57,9 +58,9 @@ class CustomViewHolder(val v: View): RecyclerView.ViewHolder(v){
init { init {
} }
} }
fun videoViews(views: Int): String{ fun videoViews(views: Long?): String{
when { when {
views<1000 -> { views!!<1000 -> {
return views.toString() return views.toString()
} }
views in 1000..999999 -> { views in 1000..999999 -> {

View File

@ -1,14 +0,0 @@
package xyz.btcland.libretube
data class Video(
val url: String,
val title: String,
val thumbnail: String,
val uploaderName: String,
val uploaderUrl:String,
val uploaderAvatar:String,
val uploadedDate: String,
val duration: Int,
val views: Int,
val uploaderVerified: Boolean
)

View File

@ -1,49 +0,0 @@
package xyz.btcland.libretube
data class VideoInPlayer(
val title: String,
val description: String,
val uploadDate: String,
val uploader: String,
val uploaderUrl: String,
val uploaderAvatar: String,
val thumbnailUrl: String,
val hls: String,
val uploaderVerified: Boolean,
val duration: Int,
val views: Int,
val likes: Int,
val dislikes: Int,
val relatedStreams: List<Video>,
val livestream: Boolean,
val proxyUrl: String,
val audioStreams: List<Stream>,
val videoStreams: List<Stream>,
val subtitles: List<Subtitle>
)
data class Stream(
val url:String,
val format:String,
val quality:String,
val mimeType:String,
val codec:String,
val videoOnly: Boolean,
val bitrate:Int,
val initStart:Int,
val initEnd:Int,
val indexStart:Int,
val indexEnd:Int,
val width:Int,
val height:Int,
val fps:Int
)
data class Subtitle(
val url:String,
val mimeType:String,
val name:String,
val code:String,
val autoGenerated:Boolean
)

View File

@ -0,0 +1,23 @@
package xyz.btcland.libretube.obj;
import java.util.List;
public class Channel {
public String id, name, avatarUrl, bannerUrl, description, nextpage;
public long subscriberCount;
public boolean verified;
public List<StreamItem> relatedStreams;
public Channel(String id, String name, String avatarUrl, String bannerUrl, String description, long subscriberCount,
boolean verified, String nextpage, List<StreamItem> relatedStreams) {
this.id = id;
this.name = name;
this.avatarUrl = avatarUrl;
this.bannerUrl = bannerUrl;
this.description = description;
this.subscriberCount = subscriberCount;
this.verified = verified;
this.nextpage = nextpage;
this.relatedStreams = relatedStreams;
}
}

View File

@ -0,0 +1,9 @@
package xyz.btcland.libretube.obj
data class ChapterSegment(
var title: String?,
var image: String?,
var start: Int?
){
constructor(): this("","",-1)
}

View File

@ -0,0 +1,22 @@
package xyz.btcland.libretube.obj;
public class Comment {
public String author, thumbnail, commentId, commentText, commentedTime, commentorUrl, repliesPage;
public int likeCount;
public boolean hearted, pinned, verified;
public Comment(String author, String thumbnail, String commentId, String commentText, String commentedTime,
String commentorUrl, String repliesPage, int likeCount, boolean hearted, boolean pinned, boolean verified) {
this.author = author;
this.thumbnail = thumbnail;
this.commentId = commentId;
this.commentText = commentText;
this.commentedTime = commentedTime;
this.commentorUrl = commentorUrl;
this.repliesPage = repliesPage;
this.likeCount = likeCount;
this.hearted = hearted;
this.pinned = pinned;
this.verified = verified;
}
}

View File

@ -0,0 +1,16 @@
package xyz.btcland.libretube.obj;
import java.util.List;
public class CommentsPage {
public List<Comment> comments;
public String nextpage;
public boolean disabled;
public CommentsPage(List<Comment> comments, String nextpage, boolean disabled) {
this.comments = comments;
this.nextpage = nextpage;
this.disabled = disabled;
}
}

View File

@ -0,0 +1,23 @@
package xyz.btcland.libretube.obj;
public class FeedItem {
public String url, title, thumbnail, uploaderUrl, uploaderName, uploaderAvatar;
public long views, duration, uploaded;
public boolean uploaderVerified;
public FeedItem(String url, String title, String thumbnail, String uploaderUrl, String uploaderName,
String uploaderAvatar, long views, long duration, long uploaded, boolean uploaderVerified) {
this.url = url;
this.title = title;
this.thumbnail = thumbnail;
this.uploaderUrl = uploaderUrl;
this.uploaderName = uploaderName;
this.uploaderAvatar = uploaderAvatar;
this.views = views;
this.duration = duration;
this.uploaded = uploaded;
this.uploaderVerified = uploaderVerified;
}
}

View File

@ -0,0 +1,20 @@
package xyz.btcland.libretube.obj
data class PipedStream(
var url: String?,
var format: String?,
var quality: String?,
var mimeType: String?,
var codec: String?,
var videoOnly: Boolean?,
var bitrate: Int?,
var initStart: Int?,
var initEnd: Int?,
var indexStart: Int?,
var indexEnd: Int?,
var width: Int?,
var height: Int?,
var fps: Int?
){
constructor(): this("","","","","",null,-1,-1,-1,-1,-1,-1,-1,-1)
}

View File

@ -0,0 +1,23 @@
package xyz.btcland.libretube.obj;
import java.util.List;
public class Playlist {
public String name, thumbnailUrl, bannerUrl, nextpage, uploader, uploaderUrl, uploaderAvatar;
public int videos;
public List<StreamItem> relatedStreams;
public Playlist(String name, String thumbnailUrl, String bannerUrl, String nextpage, String uploader,
String uploaderUrl, String uploaderAvatar, int videos, List<StreamItem> relatedStreams) {
this.name = name;
this.thumbnailUrl = thumbnailUrl;
this.bannerUrl = bannerUrl;
this.nextpage = nextpage;
this.videos = videos;
this.uploader = uploader;
this.uploaderUrl = uploaderUrl;
this.uploaderAvatar = uploaderAvatar;
this.relatedStreams = relatedStreams;
}
}

View File

@ -0,0 +1,16 @@
package xyz.btcland.libretube.obj
data class StreamItem(
var url: String?,
var title: String?,
var thumbnail: String?,
var uploaderName: String?,
var uploaderUrl: String?,
var uploaderAvatar: String?,
var uploadedDate: String?,
var duration: Long?,
var views: Long?,
var uploaderVerified: Boolean?
){
constructor() : this("","","","","","","",0,0,null)
}

View File

@ -0,0 +1,31 @@
package xyz.btcland.libretube.obj
import xyz.btcland.libretube.obj.Subtitle
data class Streams(
val title: String?,
val description: String?,
val uploadDate: String?,
val uploader: String?,
val uploaderUrl: String?,
val uploaderAvatar: String?,
val thumbnailUrl: String?,
val hls: String?,
val dash: String?,
val lbryId: String?,
val uploaderVerified: Boolean?,
val duration: Int?,
val views: Int?,
val likes: Int?,
val dislikes: Int?,
val audioStreams: List<PipedStream>?,
val videoStreams: List<PipedStream>?,
val relatedStreams: List<StreamItem>?,
val subtitles: List<Subtitle>?,
val livestream: Boolean?,
val proxyUrl: String?,
val chapters: List<ChapterSegment>?
){
constructor(): this("","","","","","","","","","",null,-1,-1,-1,-1, emptyList(), emptyList(),
emptyList(), emptyList(), null,"", emptyList())
}

View File

@ -0,0 +1,11 @@
package xyz.btcland.libretube.obj
data class Subtitle(
val url: String?,
val mimeType: String?,
val name: String?,
val code: String?,
val autoGenerated: Boolean?
){
constructor(): this("","","","",null)
}

View File

@ -0,0 +1,108 @@
<?xml version="1.0" encoding="utf-8"?>
<xyz.btcland.libretube.SingleViewTouchableMotionLayout 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"
app:layoutDescription="@xml/player_scene"
tools:context=".PlayerFragment"
android:background="@android:color/transparent"
android:id="@+id/playerMotionLayout"
>
<ScrollView
android:id="@+id/scrollView2"
android:layout_width="match_parent"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/main_container"
android:background="@color/white"
>
<LinearLayout
android:id="@+id/linLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
android:id="@+id/textTest"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text=""
/>
</LinearLayout>
</ScrollView>
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/main_container"
android:layout_width="match_parent"
android:layout_height="350dp"
android:background="#FFFFFF"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.5"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
</androidx.constraintlayout.widget.ConstraintLayout>
<xyz.btcland.libretube.CustomExoPlayerView
android:id="@+id/player"
android:layout_width="match_parent"
android:layout_height="0dp"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="@id/main_container"
app:layout_constraintStart_toStartOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container"
app:show_buffering="always"
android:background="@color/black"
/>
<ImageView
android:id="@+id/close_imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:alpha="0"
app:layout_constraintBottom_toBottomOf="@id/main_container"
app:layout_constraintEnd_toEndOf="@id/main_container"
app:layout_constraintTop_toTopOf="@id/main_container"
android:src="@drawable/ic_close"
android:visibility="gone"
/>
<ImageView
android:id="@+id/play_imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="24dp"
android:alpha="0"
android:src="@drawable/ic_play"
app:layout_constraintBottom_toBottomOf="@+id/close_imageView"
app:layout_constraintEnd_toStartOf="@+id/close_imageView"
app:layout_constraintTop_toTopOf="@+id/close_imageView"
android:visibility="gone"
/>
<TextView
android:id="@+id/title_textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="12dp"
android:alpha="0"
android:ellipsize="end"
android:maxLines="1"
app:layout_constraintBottom_toBottomOf="@+id/play_imageView"
app:layout_constraintEnd_toStartOf="@+id/play_imageView"
app:layout_constraintStart_toEndOf="@+id/player"
app:layout_constraintTop_toTopOf="@+id/play_imageView"
android:visibility="gone"
/>
</xyz.btcland.libretube.SingleViewTouchableMotionLayout>