Skip to content

Commit

Permalink
[AV][Android] Fix AV progress update (expo#7193)
Browse files Browse the repository at this point in the history
Co-Authored-By: Bartłomiej Bukowski <bartlomiejbukowski.b@gmail.com>
  • Loading branch information
mczernek and bbarthec committed Apr 15, 2020
1 parent 8e430ea commit 4d8e7e9
Show file tree
Hide file tree
Showing 5 changed files with 345 additions and 30 deletions.
29 changes: 26 additions & 3 deletions android/build.gradle
@@ -1,12 +1,23 @@
apply plugin: 'com.android.library'
apply plugin: 'maven'
apply plugin: 'kotlin-android'

group = 'host.exp.exponent'
version = '8.1.0'

// Simple helper that allows the root project to override versions declared by this library.
def safeExtGet(prop, fallback) {
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
buildscript {
// 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
}

repositories {
mavenCentral()
}

dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:${safeExtGet("kotlinVersion", "1.3.50")}")
}
}

// Upload android library to maven with javadoc and android sources
Expand Down Expand Up @@ -50,6 +61,13 @@ android {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
}

testOptions {
unitTests.all {
useJUnitPlatform()
}
}

}

if (new File(rootProject.projectDir.parentFile, 'package.json').exists()) {
Expand All @@ -75,4 +93,9 @@ dependencies {
api "com.squareup.okhttp3:okhttp-urlconnection:3.10.0"

api "com.android.support:support-annotations:${safeExtGet("supportLibVersion", "28.0.0")}"

testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.1'
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:5.5.1"

testImplementation 'io.mockk:mockk:1.9'
}
44 changes: 17 additions & 27 deletions android/src/main/java/expo/modules/av/player/PlayerData.java
Expand Up @@ -3,18 +3,19 @@
import android.content.Context;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.util.Pair;
import android.view.Surface;

import java.lang.ref.WeakReference;
import java.util.Map;

import org.unimodules.core.Promise;
import org.unimodules.core.arguments.ReadableArguments;

import java.util.Map;

import expo.modules.av.AVManagerInterface;
import expo.modules.av.AudioEventHandler;
import expo.modules.av.AudioFocusNotAcquiredException;
import expo.modules.av.progress.AndroidLooperTimeMachine;
import expo.modules.av.progress.ProgressLooper;

public abstract class PlayerData implements AudioEventHandler {
static final String STATUS_ANDROID_IMPLEMENTATION_KEY_PATH = "androidImplementation";
Expand Down Expand Up @@ -76,25 +77,8 @@ public interface FullscreenPresenter {
final Uri mUri;
final Map<String, Object> mRequestHeaders;

private Handler mHandler = new Handler();
private Runnable mProgressUpdater = new ProgressUpdater(this);

private class ProgressUpdater implements Runnable {
private WeakReference<PlayerData> mPlayerDataWeakReference;
private ProgressLooper mProgressUpdater = new ProgressLooper(new AndroidLooperTimeMachine());

private ProgressUpdater(PlayerData playerData) {
mPlayerDataWeakReference = new WeakReference<>(playerData);
}

@Override
public void run() {
final PlayerData playerData = mPlayerDataWeakReference.get();
if (playerData != null) {
playerData.callStatusUpdateListener();
playerData.progressUpdateLoop();
}
}
}

private FullscreenPresenter mFullscreenPresenter = null;
private StatusUpdateListener mStatusUpdateListener = null;
Expand Down Expand Up @@ -125,7 +109,7 @@ public static PlayerData createUnloadedPlayerData(final AVManagerInterface avMod
final Uri uri = Uri.parse(uriString);

if (status.containsKey(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH)
&& status.getString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH).equals(MediaPlayerData.IMPLEMENTATION_NAME)) {
&& status.getString(STATUS_ANDROID_IMPLEMENTATION_KEY_PATH).equals(MediaPlayerData.IMPLEMENTATION_NAME)) {
return new MediaPlayerData(avModule, context, uri, requestHeaders);
} else {
return new SimpleExoPlayerData(avModule, context, uri, uriOverridingExtension, requestHeaders);
Expand Down Expand Up @@ -161,19 +145,25 @@ final void callStatusUpdateListener() {
abstract boolean shouldContinueUpdatingProgress();

final void stopUpdatingProgressIfNecessary() {
mHandler.removeCallbacks(mProgressUpdater);
mProgressUpdater.stopLooping();
}

private void progressUpdateLoop() {
if (!shouldContinueUpdatingProgress()) {
stopUpdatingProgressIfNecessary();
} else {
mHandler.postDelayed(mProgressUpdater, mProgressUpdateIntervalMillis);
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}
}

final void beginUpdatingProgressIfNecessary() {
mHandler.post(mProgressUpdater);
mProgressUpdater.loop(mProgressUpdateIntervalMillis, () -> {
this.callStatusUpdateListener();
return null;
});
}

public final void setStatusUpdateListener(final StatusUpdateListener listener) {
Expand All @@ -198,7 +188,7 @@ final boolean shouldPlayerPlay() {
abstract void playPlayerWithRateAndMuteIfNecessary() throws AudioFocusNotAcquiredException;

abstract void applyNewStatus(final Integer newPositionMillis, final Boolean newIsLooping)
throws AudioFocusNotAcquiredException, IllegalStateException;
throws AudioFocusNotAcquiredException, IllegalStateException;

final void setStatusWithListener(final Bundle status, final SetStatusCompletionListener setStatusCompletionListener) {
if (status.containsKey(STATUS_PROGRESS_UPDATE_INTERVAL_MILLIS_KEY_PATH)) {
Expand Down
@@ -0,0 +1,13 @@
package expo.modules.av.progress

import android.os.Handler

class AndroidLooperTimeMachine : TimeMachine {

override fun scheduleAt(intervalMillis: Long, callback: TimeMachineTick) {
Handler().postDelayed(callback, intervalMillis)
}

override val time: Long
get() = System.currentTimeMillis()
}
67 changes: 67 additions & 0 deletions android/src/main/java/expo/modules/av/progress/ProgressLooper.kt
@@ -0,0 +1,67 @@
package expo.modules.av.progress

typealias TimeMachineTick = () -> Unit

interface TimeMachine {
fun scheduleAt(intervalMillis: Long, callback: TimeMachineTick)
val time: Long
}

typealias PlayerProgressListener = () -> Unit

class ProgressLooper(private val timeMachine: TimeMachine) {

private var interval = 0L
private var nextExpectedTick = -1L
private var waiting = false

private var shouldLoop: Boolean
get() = interval > 0 && nextExpectedTick >= 0 && !waiting
set(value) {
if (!value) {
interval = 0L
nextExpectedTick = -1L
waiting = false
}
}

private var listener: PlayerProgressListener? = null

fun setListener(listener: PlayerProgressListener) {
this.listener = listener
}

fun loop(interval: Long, listener: PlayerProgressListener) {
this.listener = listener
this.interval = interval
scheduleNextTick()
}

fun stopLooping() {
this.shouldLoop = false
this.listener = null
}

private fun scheduleNextTick() {
if (nextExpectedTick == -1L) {
nextExpectedTick = timeMachine.time
}
if (shouldLoop) {
nextExpectedTick += calculateNextInterval()
waiting = true
timeMachine.scheduleAt(nextExpectedTick - timeMachine.time) {
waiting = false
listener?.invoke()
scheduleNextTick()
}
}
}

private fun calculateNextInterval() =
if (nextExpectedTick > timeMachine.time) {
interval
} else {
(((timeMachine.time - nextExpectedTick) / interval) + 1) * interval
}

}

0 comments on commit 4d8e7e9

Please sign in to comment.