Skip to content

Commit

Permalink
android player
Browse files Browse the repository at this point in the history
smoother

basic caching

prep cache

somewhat works

backup

other files

android impl

blegh

lets go

touchup

add prefetch to js

use caching
  • Loading branch information
haileyok committed Apr 19, 2024
1 parent 4cb5f8a commit faae209
Show file tree
Hide file tree
Showing 13 changed files with 440 additions and 96 deletions.
95 changes: 95 additions & 0 deletions modules/expo-bluesky-video-player/android/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'maven-publish'

group = 'expo.modules.blueskyvideoplayer'
version = '0.5.0'

buildscript {
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
if (expoModulesCorePlugin.exists()) {
apply from: expoModulesCorePlugin
applyKotlinExpoModulesCorePlugin()
}

// Simple helper that allows the root project to override versions declared by this library.
ext.safeExtGet = { prop, fallback ->
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
}

// Ensures backward compatibility
ext.getKotlinVersion = {
if (ext.has("kotlinVersion")) {
ext.kotlinVersion()
} else {
ext.safeExtGet("kotlinVersion", "1.8.10")
}
}

repositories {
mavenCentral()
}

dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${getKotlinVersion()}")
}
}

afterEvaluate {
publishing {
publications {
release(MavenPublication) {
from components.release
}
}
repositories {
maven {
url = mavenLocal().url
}
}
}
}

android {
compileSdkVersion safeExtGet("compileSdkVersion", 33)

def agpVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION
if (agpVersion.tokenize('.')[0].toInteger() < 8) {
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.majorVersion
}
}

namespace "expo.modules.blueskyvideoplayer"
defaultConfig {
minSdkVersion safeExtGet("minSdkVersion", 21)
targetSdkVersion safeExtGet("targetSdkVersion", 34)
versionCode 1
versionName "0.5.0"
}
lintOptions {
abortOnError false
}
publishing {
singleVariant("release") {
withSourcesJar()
}
}
}

repositories {
mavenCentral()
}

dependencies {
implementation project(':expo-modules-core')
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:${getKotlinVersion()}"
implementation 'androidx.media3:media3-exoplayer:1.3.1'
implementation 'androidx.media3:media3-ui:1.3.1'
implementation 'com.squareup.okhttp3:okhttp:3.14.9'
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<manifest>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package expo.modules.blueskyvideoplayer

import expo.modules.kotlin.Promise
import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class ExpoBlueskyVideoPlayerModule : Module() {
override fun definition() = ModuleDefinition {
Name("ExpoBlueskyVideoPlayer")

AsyncFunction("prefetchAsync") {
{ source: String, promise: Promise ->
MediaItemManager(appContext).saveToCache(source)
promise.resolve()
}
}

View(ExpoBlueskyVideoPlayerView::class) {
Prop("source") { view: ExpoBlueskyVideoPlayerView, source: String ->
view.source = source
}

Prop("autoplay") { view: ExpoBlueskyVideoPlayerView, autoplay: Boolean ->
view.autoplay = autoplay
}

Prop("getIsPlayingAsync") { view, promise: Promise ->
promise.resolve(view.isPlaying)
}

AsyncFunction("playAsync") { view: ExpoBlueskyVideoPlayerView ->
view.play()
}

AsyncFunction("pauseAsync") { view: ExpoBlueskyVideoPlayerView ->
view.pause()
}

AsyncFunction("toggleAsync") { view: ExpoBlueskyVideoPlayerView ->
view.toggle()
}

OnViewDidUpdateProps {
val source = it.source
if (source != null) {
it.updateSource(source)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package expo.modules.blueskyvideoplayer

import android.content.Context
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import expo.modules.kotlin.AppContext
import expo.modules.kotlin.views.ExpoView

class ExpoBlueskyVideoPlayerView(context: Context, appContext: AppContext) : ExpoView(context, appContext) {
private var playerView: PlayerView
private var player: ExoPlayer

var autoplay: Boolean = true
var source: String? = null
var isPlaying = true

init {
this.playerView = PlayerView(context).apply {
layoutParams = LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT)
useController = false
}
this.player = ExoPlayer.Builder(context).build().also {
playerView.player = it
}.apply {
repeatMode = ExoPlayer.REPEAT_MODE_ONE
volume = 0f
}
this.addView(this.playerView)
}

override fun onAttachedToWindow() {
if (this.autoplay && this.isPlaying) {
this.player.play()
}
super.onAttachedToWindow()
}

override fun onDetachedFromWindow() {
this.player.pause()
super.onDetachedFromWindow()
}

fun updateSource(source: String) {
val mediaItem = MediaItemManager(appContext).getItem(source)
player.setMediaItem(mediaItem)
player.prepare()
player.playWhenReady = true
}

fun play() {
this.player.play()
this.isPlaying = true
}

fun pause() {
this.player.pause()
this.isPlaying = false
}

fun toggle() {
if (this.isPlaying) {
this.player.pause()
this.isPlaying = false
} else {
this.player.play()
this.isPlaying = true
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package expo.modules.blueskyvideoplayer

import android.net.Uri
import androidx.media3.common.MediaItem
import expo.modules.kotlin.AppContext
import okhttp3.OkHttpClient
import okhttp3.Request
import okio.IOException
import java.io.File
import java.io.FileOutputStream
import java.security.MessageDigest

class MediaItemManager(private val appContext: AppContext) {
companion object {
private val client = OkHttpClient()
}

fun getItem(source: String): MediaItem {
val path = createPath(source)
return if (File(path).exists()) {
MediaItem.fromUri(Uri.parse(path))
} else {
saveToCache(source)
MediaItem.fromUri(Uri.parse(source))
}
}

fun saveToCache(source: String) {
val request = Request.Builder()
.url(source)
.build()

client.newCall(request).enqueue(object : okhttp3.Callback {
override fun onFailure(call: okhttp3.Call, e: IOException) {
e.printStackTrace() // Handle the error
}

override fun onResponse(call: okhttp3.Call, response: okhttp3.Response) {
val path = createPath(source)
if (response.isSuccessful) {
val file = File(path)
val fos = FileOutputStream(file)
val inputStream = response.body?.byteStream()

inputStream?.use { input ->
fos.use { fileOut ->
input.copyTo(fileOut)
}
}
}
}
})
}

private fun getHash(source: String): String {
val md = MessageDigest.getInstance("SHA-1")
val byteArray = md.digest(source.toByteArray())
return byteArray.joinToString("") { "%02x".format(it) }
}

private fun getGifsDirectory(): String {
val gifsDirectory = appContext.cacheDirectory.path + "/gifs"
if (!File(gifsDirectory).exists()) {
File(gifsDirectory).mkdir()
}
return gifsDirectory
}

private fun createPath(source: String): String {
return getGifsDirectory() + "/" + getHash(source)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,9 @@ public class ExpoBlueskyVideoPlayerModule: Module {
public func definition() -> ModuleDefinition {
Name("ExpoBlueskyVideoPlayer")

AsyncFunction("setShouldAutoplayAsync") { (value: Bool) in

}

AsyncFunction("prefetchAsync") { (source: String) in
PlayerItemManager.shared.getOrAddItem(source: source)
AsyncFunction("prefetchAsync") { (source: String, promise: Promise) in
PlayerItemManager.shared.saveToCache(source: source)
promise.resolve()
}

View(ExpoBlueskyVideoPlayerView.self) {
Expand All @@ -19,16 +16,27 @@ public class ExpoBlueskyVideoPlayerModule: Module {
view.source = prop
}

Prop("autoplay") { (view: ExpoBlueskyVideoPlayerView, prop: Bool) in
view.autoplay = prop
}

AsyncFunction("getIsPlayingAsync") { (view: ExpoBlueskyVideoPlayerView, promise: Promise) in
promise.resolve(view.isPlaying)
}

AsyncFunction("playAsync") { (view: ExpoBlueskyVideoPlayerView) in
AsyncFunction("toggleAsync") { (view: ExpoBlueskyVideoPlayerView, promise: Promise) in
view.toggle()
promise.resolve()
}

AsyncFunction("playAsync") { (view: ExpoBlueskyVideoPlayerView, promise: Promise) in
view.play()
promise.resolve()
}

AsyncFunction("pauseAsync") { (view: ExpoBlueskyVideoPlayerView) in
AsyncFunction("pauseAsync") { (view: ExpoBlueskyVideoPlayerView, promise: Promise) in
view.pause()
promise.resolve()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
import ExpoModulesCore

public class ExpoBlueskyVideoPlayerView: ExpoView, AVPlayerViewControllerDelegate {
public var source: String? = nil
public var isPlaying: Bool = true
public var source: String? = nil {
didSet {
if self.controller != nil, let source = source {
self.controller?.initForView(source, view: self)
}
}
}
public var isPlaying = true
public var autoplay = true

private var controller: PlayerController? = nil

Expand All @@ -25,12 +32,11 @@ public class ExpoBlueskyVideoPlayerView: ExpoView, AVPlayerViewControllerDelegat
return
}

if let controller = PlayerControllerManager.shared.getPlayer() {
controller.setFrame(rect: bounds)
controller.initForView(source, view: self)
self.addSubview(controller.view)
self.controller = controller
}
let controller = PlayerControllerManager.shared.getController()
controller.setFrame(rect: bounds)
controller.initForView(source, view: self)
self.addSubview(controller.view)
self.controller = controller
} else {
self.controller?.release()
}
Expand All @@ -45,4 +51,12 @@ public class ExpoBlueskyVideoPlayerView: ExpoView, AVPlayerViewControllerDelegat
self.isPlaying = false
self.controller?.pause()
}

func toggle() {
if self.isPlaying {
self.pause()
} else {
self.play()
}
}
}
Loading

0 comments on commit faae209

Please sign in to comment.