Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Background playback improvements #104

Closed
wants to merge 10 commits into from
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 6 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@
android:name=".SettingsActivity"
android:label="@string/title_activity_settings"/>
<service android:name=".BackgroundPlaybackService" android:exported="false" />
<receiver android:name=".NotificationButtonReceiver" android:exported="false">
<intent-filter>
<action android:name="is.xyz.mpv.ACTION_PREV" />
<action android:name="is.xyz.mpv.ACTION_NEXT" />
</intent-filter>
</receiver>
</application>

</manifest>
78 changes: 66 additions & 12 deletions app/src/main/java/is/xyz/mpv/BackgroundPlaybackService.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.IBinder;
import android.util.Log;

Expand All @@ -22,29 +23,69 @@ public class BackgroundPlaybackService extends Service implements EventObserver
public void onCreate() {
MPVLib.addObserver(this);
MPVLib.observeProperty("media-title", MPVLib.mpvFormat.MPV_FORMAT_STRING);
MPVLib.observeProperty("metadata/by-key/Artist", MPVLib.mpvFormat.MPV_FORMAT_STRING);
MPVLib.observeProperty("metadata/by-key/Album", MPVLib.mpvFormat.MPV_FORMAT_STRING);
}

private Notification buildNotification(String contentText) {
private String cachedMediaTitle;
private String cachedMediaArtist;
private String cachedMediaAlbum;
private boolean shouldShowPrevNext;

private boolean isNullOrEmpty(String s) {
return s == null || s.isEmpty();
}

private PendingIntent createButtonIntent(String action) {
Intent intent = new Intent();
intent.setAction("is.xyz.mpv." + action);
return PendingIntent.getBroadcast(this, 0, intent, 0);
}

private Notification buildNotification() {
Intent notificationIntent = new Intent(this, MPVActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0);

Notification.Builder builder =
new Notification.Builder(this)
.setPriority(Notification.PRIORITY_LOW)
.setContentTitle(getText(R.string.mpv_activity))
.setContentText(contentText)
.setSmallIcon(R.drawable.ic_play_arrow_black_24dp)
.setVisibility(Notification.VISIBILITY_PUBLIC)
.setContentTitle(cachedMediaTitle)
.setSmallIcon(R.drawable.ic_mpv_symbolic)
.setContentIntent(pendingIntent);
if (thumbnail != null)
builder.setLargeIcon(thumbnail);
if (!isNullOrEmpty(cachedMediaAlbum) && !isNullOrEmpty(cachedMediaAlbum))
builder.setContentText(cachedMediaArtist + " / " + cachedMediaAlbum);
else if (!isNullOrEmpty(cachedMediaArtist))
builder.setContentText(cachedMediaAlbum);
else if (!isNullOrEmpty(cachedMediaAlbum))
builder.setContentText(cachedMediaArtist);
if (shouldShowPrevNext) {
// action icons need to be 32dp according to the docs
builder.addAction(R.drawable.ic_skip_previous_black_32dp, "Prev", createButtonIntent("ACTION_PREV"));
builder.addAction(R.drawable.ic_skip_next_black_32dp, "Next", createButtonIntent("ACTION_NEXT"));
builder.setStyle(new Notification.MediaStyle().setShowActionsInCompactView(0, 1));
}

return builder.build();
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v(TAG, "BackgroundPlaybackService: starting");

// read some metadata

cachedMediaTitle = MPVLib.getPropertyString("media-title");
cachedMediaArtist = MPVLib.getPropertyString("metadata/by-key/Artist");
cachedMediaAlbum = MPVLib.getPropertyString("metadata/by-key/Album");
Integer tmp = MPVLib.getPropertyInt("playlist-count");
shouldShowPrevNext = tmp != null && tmp > 1;

// create notification and turn this into a "foreground service"

Notification notification = buildNotification(MPVLib.getPropertyString("media-title"));
Notification notification = buildNotification();
startForeground(NOTIFICATION_ID, notification);

// resume playback (audio-only)
Expand All @@ -65,6 +106,13 @@ public void onDestroy() {
@Override
public IBinder onBind(Intent intent) { return null; }

/* This is called by MPVActivity to give us a thumbnail to display
alongside the permanent notification */
private static Bitmap thumbnail = null;
public static void setThumbnail(Bitmap b) {
thumbnail = b;
}

/* Event observers */

@Override
Expand All @@ -78,13 +126,19 @@ public void eventProperty(@NotNull String property, boolean value) {}

@Override
public void eventProperty(@NotNull String property, @NotNull String value) {
if (property.equals("media-title")) {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null) {
notificationManager.notify(NOTIFICATION_ID, buildNotification(value));
}
}
if (property.equals("media-title"))
cachedMediaTitle = value;
else if (property.equals("metadata/by-key/Artist"))
cachedMediaArtist = value;
else if (property.equals("metadata/by-key/Album"))
cachedMediaAlbum = value;
else
return;

NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
if (notificationManager != null)
notificationManager.notify(NOTIFICATION_ID, buildNotification());
}

@Override
Expand Down
6 changes: 6 additions & 0 deletions app/src/main/java/is/xyz/mpv/MPVActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,10 @@ class MPVActivity : Activity(), EventObserver, TouchGesturesObserver {

override fun onPause() {
val shouldBackground = shouldBackground()
if (shouldBackground && !MPVLib.getPropertyString("video-format").isNullOrEmpty())
BackgroundPlaybackService.setThumbnail(MPVLib.grabThumbnail(THUMB_SIZE))
else
BackgroundPlaybackService.setThumbnail(null)
player.onPause()
super.onPause()

Expand Down Expand Up @@ -668,6 +672,8 @@ class MPVActivity : Activity(), EventObserver, TouchGesturesObserver {
private val CONTROLS_DISPLAY_TIMEOUT = 2000L
// how far to seek backward/forward with (currently) TV remote buttons
private val BUTTON_SEEK_RANGE = 10
// size (px) of the thumbnail displayed with background play notification
private val THUMB_SIZE = 192
}
}

Expand Down
3 changes: 3 additions & 0 deletions app/src/main/java/is/xyz/mpv/MPVLib.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLSurfaceView;
import android.view.Surface;

Expand All @@ -27,6 +28,8 @@ public class MPVLib {

public static native int setOptionString(String name, String value);

public static native Bitmap grabThumbnail(int dimension);

public static native Integer getPropertyInt(String property);
public static native void setPropertyInt(String property, Integer value);
public static native Boolean getPropertyBoolean(String property);
Expand Down
21 changes: 21 additions & 0 deletions app/src/main/java/is/xyz/mpv/NotificationButtonReceiver.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package `is`.xyz.mpv

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.util.Log

class NotificationButtonReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
Log.v(TAG, "NotificationButtonReceiver: ${intent!!.action}")
when(intent!!.action) {
"$PREFIX.ACTION_PREV" -> MPVLib.command(arrayOf("playlist-prev"))
"$PREFIX.ACTION_NEXT" -> MPVLib.command(arrayOf("playlist-next"))
}
}

companion object {
private val TAG = "mpv"
private val PREFIX = "is.xyz.mpv"
}
}
3 changes: 2 additions & 1 deletion app/src/main/jni/Android.mk
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ LOCAL_SRC_FILES := \
log.cpp \
jni_utils.cpp \
property.cpp \
event.cpp
event.cpp \
thumbnail.cpp
LOCAL_LDLIBS := -llog -lGLESv3 -lEGL -latomic
LOCAL_SHARED_LIBRARIES := swresample avutil avcodec avformat swscale avfilter avdevice mpv

Expand Down
11 changes: 11 additions & 0 deletions app/src/main/jni/jni_utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ jclass java_Integer, java_Boolean;
jmethodID java_Integer_init, java_Integer_intValue, java_Boolean_init, java_Boolean_booleanValue;
jmethodID java_GLSurfaceView_requestRender;

jclass android_graphics_Bitmap, android_graphics_Bitmap_Config;
jmethodID android_graphics_Bitmap_createBitmap;
jfieldID android_graphics_Bitmap_Config_ARGB_8888;

jclass mpv_MPVLib;
jmethodID mpv_MPVLib_eventProperty_S, mpv_MPVLib_eventProperty_Sb, mpv_MPVLib_eventProperty_Sl, mpv_MPVLib_eventProperty_SS, mpv_MPVLib_event;

Expand All @@ -35,6 +39,13 @@ void init_methods_cache(JNIEnv *env) {
java_Boolean_init = env->GetMethodID(java_Boolean, "<init>", "(Z)V");
java_Boolean_booleanValue = env->GetMethodID(java_Boolean, "booleanValue", "()Z");

android_graphics_Bitmap = FIND_CLASS("android/graphics/Bitmap");
// createBitmap(int[], int, int, android.graphics.Bitmap$Config)
android_graphics_Bitmap_createBitmap = env->GetStaticMethodID(android_graphics_Bitmap, "createBitmap", "([IIILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;");
android_graphics_Bitmap_Config = FIND_CLASS("android/graphics/Bitmap$Config");
// static final android.graphics.Bitmap$Config ARGB_8888
android_graphics_Bitmap_Config_ARGB_8888 = env->GetStaticFieldID(android_graphics_Bitmap_Config, "ARGB_8888", "Landroid/graphics/Bitmap$Config;");

mpv_MPVLib = FIND_CLASS("is/xyz/mpv/MPVLib");
mpv_MPVLib_eventProperty_S = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;)V"); // eventProperty(String)
mpv_MPVLib_eventProperty_Sb = env->GetStaticMethodID(mpv_MPVLib, "eventProperty", "(Ljava/lang/String;Z)V"); // eventProperty(String, boolean)
Expand Down
4 changes: 4 additions & 0 deletions app/src/main/jni/jni_utils.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ extern jclass java_Integer, java_Boolean;
extern jmethodID java_Integer_init, java_Integer_intValue, java_Boolean_init, java_Boolean_booleanValue;
extern jmethodID java_GLSurfaceView_requestRender;

extern jclass android_graphics_Bitmap, android_graphics_Bitmap_Config;
extern jmethodID android_graphics_Bitmap_createBitmap;
extern jfieldID android_graphics_Bitmap_Config_ARGB_8888;

extern jclass mpv_MPVLib;
extern jmethodID mpv_MPVLib_eventProperty_S, mpv_MPVLib_eventProperty_Sb, mpv_MPVLib_eventProperty_Sl, mpv_MPVLib_eventProperty_SS, mpv_MPVLib_event;
131 changes: 131 additions & 0 deletions app/src/main/jni/thumbnail.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
#include <stdlib.h>
#include <vector>
#include <string>

#include <jni.h>
#include <android/bitmap.h>
#include <mpv/client.h>

extern "C" {
#include <libswscale/swscale.h>
};

#include "jni_utils.h"
#include "globals.h"
#include "log.h"

extern "C" {
jni_func(jobject, grabThumbnail, jint dimension);
};

jni_func(jobject, grabThumbnail, jint dimension) {
ALOGV("grabbing thumbnail\n");

mpv_node result;
{
mpv_node c, c_arg0, c_arg1;
mpv_node c_args[2];
mpv_node_list c_array;
c_arg0.format = MPV_FORMAT_STRING;
c_arg0.u.string = (char*) "screenshot-raw";
c_args[0] = c_arg0;
c_arg1.format = MPV_FORMAT_STRING;
c_arg1.u.string = (char*) "video";
c_args[1] = c_arg1;
c_array.num = 2;
c_array.values = c_args;
c.format = MPV_FORMAT_NODE_ARRAY;
c.u.list = &c_array;
if (mpv_command_node(g_mpv, &c, &result) < 0)
return NULL;
}

unsigned w, h;
w = h = 0;
struct mpv_byte_array *data = NULL;
{
if (result.format != MPV_FORMAT_NODE_MAP)
return NULL;
for (int i = 0; i < result.u.list->num; i++) {
std::string key(result.u.list->keys[i]);
const mpv_node *val = &result.u.list->values[i];
if (key == "w" || key == "h") {
if (val->format != MPV_FORMAT_INT64)
return NULL;
if (key == "w")
w = val->u.int64;
else
h = val->u.int64;
} else if (key == "format") {
if (val->format != MPV_FORMAT_STRING)
return NULL;
if (strcmp(val->u.string, "bgr0"))
return NULL;
} else if (key == "data") {
if (val->format != MPV_FORMAT_BYTE_ARRAY)
return NULL;
data = val->u.ba;
}
}
}
if (!w || !h || !data)
return NULL;
ALOGV("screenshot w:%u h:%u\n", w, h);

// crop to square
unsigned crop_left, crop_right, crop_top, crop_bottom;
if (w > h) {
crop_top = crop_bottom = 0;
int tmp = w - h;
crop_left = tmp / 2;
crop_right = tmp / 2 + tmp % 2;
} else {
crop_left = crop_right = 0;
int tmp = h - w;
crop_top = tmp / 2;
crop_bottom = tmp / 2 + tmp % 2;
}
unsigned new_w, new_h;
new_w = w - crop_left - crop_right;
new_h = h - crop_top - crop_bottom;
ALOGV("cropped w:%u h:%u\n", new_w, new_h);

std::vector<uint32_t> new_data;
new_data.reserve(new_w * new_h);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can't use "new" vector members after you call reserve, you need to do resize, or just pass the size as argument to the constructor.

for (int y = 0; y < new_h; y++) {
for (int x = 0; x < new_w; x++) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can the inner loop be replaced with memcpy?

int tx = x + crop_left, ty = y + crop_top;
new_data[y*new_w + x] = ((uint32_t*) data->data)[ty*w + tx];
}
}
mpv_free_node_contents(&result); // frees data->data

// convert & scale to appropriate size
struct SwsContext *ctx = sws_getContext(
new_w, new_h, AV_PIX_FMT_BGR0,
dimension, dimension, AV_PIX_FMT_RGB32,
SWS_BICUBIC, NULL, NULL, NULL);
if (!ctx)
return NULL;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're leaking new_data here. Probably better to replace with std::vector or similar so that RAII can be utilized.

int src_stride = sizeof(uint32_t) * new_w, dst_stride = sizeof(uint32_t) * dimension;
std::vector<uint8_t> scaled;
scaled.reserve(dimension * dst_stride);
uint8_t *src_p[] = { (uint8_t*) new_data.data() }, *dst_p[] = { scaled.data() };
sws_scale(ctx, src_p, &src_stride, 0, new_h, dst_p, &dst_stride);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All arrays you pass to sws_scale have to be 4 elements, it's a bug/undocumented in ffmpeg, so make sure src/dst/stride have 4 elements.

sws_freeContext(ctx);


// create android.graphics.Bitmap
jintArray arr = env->NewIntArray(dimension * dimension);
env->SetIntArrayRegion(arr, 0, dimension * dimension, (jint*) scaled.data());

jobject bitmap_config =
env->GetStaticObjectField(android_graphics_Bitmap_Config, android_graphics_Bitmap_Config_ARGB_8888);
jobject bitmap =
env->CallStaticObjectMethod(android_graphics_Bitmap, android_graphics_Bitmap_createBitmap,
arr, dimension, dimension, bitmap_config);
env->DeleteLocalRef(arr);
env->DeleteLocalRef(bitmap_config);

return bitmap;
}
14 changes: 14 additions & 0 deletions app/src/main/res/drawable/ic_mpv_symbolic.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<!-- imported from <mpv>/etc/mpv-symbolic.svg -->
<path android:fillAlpha="1" android:fillColor="#000000"
android:fillType="nonZero"
android:pathData="M32.58,2.91A27.95,27.95 0,0 0,4.63 30.86A27.95,27.95 0,0 0,32.58 58.81A27.95,27.95 0,0 0,60.53 30.86A27.95,27.95 0,0 0,32.58 2.91zM33.31,4.05A25.95,25.95 0,0 1,59.26 30.01A25.95,25.95 0,0 1,33.31 55.96A25.95,25.95 0,0 1,7.36 30.01A25.95,25.95 0,0 1,33.31 4.05z"
android:strokeAlpha="0.99215686" android:strokeColor="#00000000"
android:strokeLineCap="round" android:strokeLineJoin="bevel" android:strokeWidth="0.10161044"/>
<path android:fillAlpha="1" android:fillColor="#000000"
android:fillType="evenOdd"
android:pathData="m32.21,18.13a12.85,12.85 0,0 0,-12.85 12.85,12.85 12.85,0 0,0 12.85,12.85 12.85,12.85 0,0 0,12.85 -12.85,12.85 12.85,0 0,0 -12.85,-12.85zM28.96,25.21 L38.17,30.79 28.96,36.66 28.96,25.21zM54.81,28.29a20,20 0,0 1,-20 20,20 20,0 0,1 -20,-20 20,20 0,0 1,20 -20,20 20,0 0,1 20,20z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="0.1"/>
</vector>
4 changes: 4 additions & 0 deletions app/src/main/res/drawable/ic_skip_next_black_32dp.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,18l8.5,-6L6,6v12zM16,6v12h2V6h-2z"/>
</vector>
4 changes: 4 additions & 0 deletions app/src/main/res/drawable/ic_skip_previous_black_32dp.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<vector android:height="32dp" android:viewportHeight="24.0"
android:viewportWidth="24.0" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M6,6h2v12L6,18zM9.5,12l8.5,6L18,6z"/>
</vector>