hook up device events

This commit is contained in:
Enno Boland 2024-10-04 17:42:05 +02:00
parent 68b064703e
commit d0eb577079
13 changed files with 438 additions and 18 deletions

View file

@ -43,6 +43,14 @@ enum sc_control_msg_type {
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
SC_CONTROL_MSG_TYPE_START_APP,
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
SC_CONTROL_MSG_TYPE_MEDIA_STATE,
SC_CONTROL_MSG_TYPE_MEDIA_SEEK,
};
enum sc_screen_power_mode {
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
SC_SCREEN_POWER_MODE_OFF = 0,
SC_SCREEN_POWER_MODE_NORMAL = 2,
};
enum sc_copy_key {
@ -111,6 +119,14 @@ struct sc_control_msg {
struct {
char *name;
} start_app;
struct {
uint16_t player_id;
uint64_t position;
} media_seek;
struct {
uint16_t player_id;
uint8_t state;
} media_state;
};
};

View file

@ -7,6 +7,20 @@
#include "util/binary.h"
#include "util/log.h"
static int read_message(uint8_t **target, const uint8_t *src, const uint16_t size) {
uint8_t *data = malloc(size + 1);
if (!data) {
LOG_OOM();
return -1;
}
if (size) {
data[size] = '\0';
memcpy(data, src, size);
}
*target = data;
return 0;
}
ssize_t
sc_device_msg_deserialize(const uint8_t *buf, size_t len,
struct sc_device_msg *msg) {
@ -25,17 +39,10 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
if (clipboard_len > len - 5) {
return 0; // no complete message
}
char *text = malloc(clipboard_len + 1);
if (!text) {
LOG_OOM();
if (read_message((uint8_t **)&msg->clipboard.text, &buf[5], clipboard_len) == -1) {
return -1;
}
if (clipboard_len) {
memcpy(text, &buf[5], clipboard_len);
}
text[clipboard_len] = '\0';
msg->clipboard.text = text;
return 5 + clipboard_len;
}
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: {
@ -56,20 +63,42 @@ sc_device_msg_deserialize(const uint8_t *buf, size_t len,
if (size < len - 5) {
return 0; // not available
}
uint8_t *data = malloc(size);
if (!data) {
LOG_OOM();
return -1;
}
if (size) {
memcpy(data, &buf[5], size);
}
msg->uhid_output.id = id;
msg->uhid_output.size = size;
msg->uhid_output.data = data;
if (read_message(&msg->uhid_output.data, &buf[5], size) == -1) {
return -1;
}
return 5 + size;
case DEVICE_MSG_TYPE_MEDIA_UPDATE: {
if (len < 5) {
// at least id + size
return 0; // not available
}
uint16_t id = sc_read16be(&buf[1]);
size_t size = sc_read16be(&buf[3]);
if (size < len - 5) {
return 0; // not available
}
msg->media_update.id = id;
msg->media_update.size = size;
if (read_message(&msg->media_update.data, &buf[5], size) == -1) {
return -1;
}
return 5 + size;
}
case DEVICE_MSG_TYPE_MEDIA_REMOVE: {
if (len < 3) {
// at least id
return 0; // not available
}
uint16_t id = sc_read16be(&buf[1]);
msg->media_remove.id = id;
return 3;
}
}
default:
LOGW("Unknown device message type: %d", (int) msg->type);
@ -86,6 +115,9 @@ sc_device_msg_destroy(struct sc_device_msg *msg) {
case DEVICE_MSG_TYPE_UHID_OUTPUT:
free(msg->uhid_output.data);
break;
case DEVICE_MSG_TYPE_MEDIA_UPDATE:
free(msg->media_update.data);
break;
default:
// nothing to do
break;

View file

@ -15,6 +15,8 @@ enum sc_device_msg_type {
DEVICE_MSG_TYPE_CLIPBOARD,
DEVICE_MSG_TYPE_ACK_CLIPBOARD,
DEVICE_MSG_TYPE_UHID_OUTPUT,
DEVICE_MSG_TYPE_MEDIA_UPDATE,
DEVICE_MSG_TYPE_MEDIA_REMOVE,
};
struct sc_device_msg {
@ -31,6 +33,14 @@ struct sc_device_msg {
uint16_t size;
uint8_t *data; // owned, to be freed by free()
} uhid_output;
struct {
uint16_t id;
uint16_t size;
uint8_t *data; // owned, to be freed by free()
} media_update;
struct {
uint16_t id;
} media_remove;
};
};

View file

@ -73,6 +73,22 @@ task_uhid_output(void *userdata) {
free(data);
}
static void
dump_media_update(const struct sc_device_msg* msg) {
uint8_t msg_type = 0;
uint8_t *msg_ptr = NULL;
for (int i = 0; i < msg->media_update.size; i++) {
if (msg_ptr == NULL) {
msg_type = msg->media_update.data[i];
msg_ptr = &msg->media_update.data[i + 1];
} else if (msg->media_update.data[i] == 0) {
LOGI("Media update: %i, %s", (int)msg_type, msg_ptr);
msg_ptr = NULL;
msg_type = 0;
}
}
}
static void
process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
switch (msg->type) {
@ -149,6 +165,12 @@ process_msg(struct sc_receiver *receiver, struct sc_device_msg *msg) {
return;
}
break;
case DEVICE_MSG_TYPE_MEDIA_UPDATE:
dump_media_update(msg);
break;
case DEVICE_MSG_TYPE_MEDIA_REMOVE:
LOGI("Media remove: %i", msg->media_remove.id);
break;
}
}

View file

@ -26,6 +26,7 @@ public class Options {
private int scid = -1; // 31-bit non-negative value, or -1
private boolean video = true;
private boolean audio = true;
private boolean mediaControl = true;
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
@ -96,6 +97,10 @@ public class Options {
return audio;
}
public boolean getMediaControls() {
return mediaControl;
}
public int getMaxSize() {
return maxSize;
}

View file

@ -23,8 +23,12 @@ import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import com.genymobile.scrcpy.wrappers.MediaManager;
import android.annotation.SuppressLint;
import android.media.MediaMetadata;
import android.media.session.PlaybackState;
import android.os.BatteryManager;
import android.os.Build;
import android.os.Looper;
@ -95,6 +99,7 @@ public final class Server {
boolean control = options.getControl();
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean media = options.getMediaControls();
boolean sendDummyByte = options.getSendDummyByte();
Workarounds.apply();
@ -113,6 +118,40 @@ public final class Server {
ControlChannel controlChannel = connection.getControlChannel();
controller = new Controller(controlChannel, cleanUp, options);
asyncProcessors.add(controller);
if (media) {
MediaManager mediaManager = MediaManager.create();
mediaManager.setMediaChangeListener(new MediaManager.MediaChange() {
@Override
public void onMetadataChange(int id, MediaMetadata metadata) {
Ln.i("onMetadataChange " + id);
byte[] data = MediaManager.mediaMetadataSerialize(metadata);
DeviceMessage msg = DeviceMessage.createMediaUpdate(id, data);
controller.getSender().send(msg);
}
@Override
public void onPlaybackStateChange(int id, PlaybackState playbackState) {
Ln.i("onPlaybackStateChange " + id);
int state = MediaManager.create().playbackStateSerialize(playbackState);
if(state < 0) {
return;
}
DeviceMessage msg = DeviceMessage.createMediaState(id, state);
controller.getSender().send(msg);
}
@Override
public void onRemove(int id) {
Ln.i("onRemove " + id);
DeviceMessage msg = DeviceMessage.createMediaRemove(id);
controller.getSender().send(msg);
}
});
mediaManager.start();
}
}
if (audio) {
@ -158,6 +197,9 @@ public final class Server {
}
}
Completion completion = new Completion(asyncProcessors.size());
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.start((fatalError) -> {

View file

@ -25,6 +25,8 @@ public final class ControlMessage {
public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15;
public static final int TYPE_START_APP = 16;
public static final int TYPE_RESET_VIDEO = 17;
public static final int TYPE_MEDIA_STATE = 18;
public static final int TYPE_MEDIA_SEEK = 19;
public static final long SEQUENCE_INVALID = 0;
@ -50,9 +52,15 @@ public final class ControlMessage {
private long sequence;
private int id;
private byte[] data;
<<<<<<< HEAD
private boolean on;
private int vendorId;
private int productId;
=======
private int mediaState;
private long mediaSeek;
>>>>>>> 38b99700 (hook up device events)
private ControlMessage() {
}
@ -159,10 +167,26 @@ public final class ControlMessage {
return msg;
}
<<<<<<< HEAD
public static ControlMessage createStartApp(String name) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_START_APP;
msg.text = name;
=======
public static ControlMessage createMediaState(int receiverId, byte state) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_MEDIA_STATE;
msg.id = receiverId;
msg.mediaState = state;
return msg;
}
public static ControlMessage createMediaSeek(int receiverId, long position) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_MEDIA_STATE;
msg.id = receiverId;
msg.mediaSeek = position;
>>>>>>> 38b99700 (hook up device events)
return msg;
}
@ -238,6 +262,7 @@ public final class ControlMessage {
return data;
}
<<<<<<< HEAD
public boolean getOn() {
return on;
}
@ -248,5 +273,13 @@ public final class ControlMessage {
public int getProductId() {
return productId;
=======
public long getMediaSeek() {
return mediaSeek;
}
public int getMediaState() {
return mediaState;
>>>>>>> 38b99700 (hook up device events)
}
}

View file

@ -56,11 +56,27 @@ public class ControlMessageReader {
return parseUhidDestroy();
case ControlMessage.TYPE_START_APP:
return parseStartApp();
case ControlMessage.TYPE_MEDIA_STATE:
return parseMediaPlayStateRequest();
case ControlMessage.TYPE_MEDIA_SEEK:
return parseMediaSeekRequest();
default:
throw new ControlProtocolException("Unknown event type: " + type);
}
}
private ControlMessage parseMediaSeekRequest() throws IOException {
int receiverId = dis.readUnsignedShort();
long position = dis.readLong();
return ControlMessage.createMediaSeek(receiverId, position);
}
private ControlMessage parseMediaPlayStateRequest() throws IOException {
int receiverId = dis.readUnsignedShort();
byte state = dis.readByte();
return ControlMessage.createMediaState(receiverId, state);
}
private ControlMessage parseInjectKeycode() throws IOException {
int action = dis.readUnsignedByte();
int keycode = dis.readInt();

View file

@ -330,6 +330,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
break;
case ControlMessage.TYPE_RESET_VIDEO:
resetVideo();
case ControlMessage.TYPE_MEDIA_STATE:
mediaUpdateState(msg.getId(), msg.getMediaState());
break;
case ControlMessage.TYPE_MEDIA_SEEK:
mediaSeek(msg.getId(), msg.getMediaSeek());
break;
default:
// do nothing
@ -338,6 +343,14 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
return true;
}
private void mediaUpdateState(int id, int mediaState) {
// TODO
}
private void mediaSeek(int id, long mediaSeek) {
// TODO
}
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
assert displayId != Device.DISPLAY_ID_NONE;

View file

@ -5,12 +5,16 @@ public final class DeviceMessage {
public static final int TYPE_CLIPBOARD = 0;
public static final int TYPE_ACK_CLIPBOARD = 1;
public static final int TYPE_UHID_OUTPUT = 2;
public static final int TYPE_MEDIA_UPDATE = 3;
public static final int TYPE_MEDIA_REMOVE = 4;
public static final int TYPE_MEDIA_STATE = 4;
private int type;
private String text;
private long sequence;
private int id;
private byte[] data;
private int mediaState;
private DeviceMessage() {
}
@ -37,6 +41,29 @@ public final class DeviceMessage {
return event;
}
public static DeviceMessage createMediaUpdate(int id, byte[] data) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_MEDIA_UPDATE;
event.id = id;
event.data = data;
return event;
}
public static DeviceMessage createMediaState(int id, int state) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_MEDIA_STATE;
event.id = id;
event.mediaState = state;
return event;
}
public static DeviceMessage createMediaRemove(int id) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_MEDIA_REMOVE;
event.id = id;
return event;
}
public int getType() {
return type;
}

View file

@ -22,6 +22,7 @@ public class DeviceMessageWriter {
public void write(DeviceMessage msg) throws IOException {
int type = msg.getType();
dos.writeByte(type);
byte[] data;
switch (type) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
@ -35,10 +36,19 @@ public class DeviceMessageWriter {
break;
case DeviceMessage.TYPE_UHID_OUTPUT:
dos.writeShort(msg.getId());
byte[] data = msg.getData();
data = msg.getData();
dos.writeShort(data.length);
dos.write(data);
break;
case DeviceMessage.TYPE_MEDIA_UPDATE:
dos.writeShort(msg.getId());
data = msg.getData();
dos.writeShort(data.length);
dos.write(data);
break;
case DeviceMessage.TYPE_MEDIA_REMOVE:
dos.writeShort(msg.getId());
break;
default:
throw new ControlProtocolException("Unknown event type: " + type);
}

View file

@ -0,0 +1,186 @@
package com.genymobile.scrcpy.wrappers;
import android.media.MediaMetadata;
import android.media.session.MediaController;
import android.media.session.MediaSessionManager;
import android.media.session.PlaybackState;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
public class MediaManager {
final static byte KEY_DURATION = 0;
final static byte KEY_ALBUM = 1;
final static byte KEY_ARTIST = 2;
final static byte KEY_TITLE = 3;
final static byte STATE_PLAYING = 0;
final static byte STATE_STOPPED = 1;
final static byte STATE_PAUSED = 2;
MediaSessionManager sessionManager;
private MediaChange mediaChangeListener;
boolean started = false;
public interface MediaChange {
void onMetadataChange(int id, MediaMetadata metadata);
void onPlaybackStateChange(int id, PlaybackState playbackState);
void onRemove(int id);
}
public static MediaManager create() {
MediaSessionManager manager = FakeContext.get().getSystemService(MediaSessionManager.class);
return new MediaManager(manager);
}
int nextId = 0;
HashMap<String, Integer> idMap = new HashMap<>();
List<MediaController> mediaControllers = Collections.emptyList();
private MediaManager(MediaSessionManager sessionManager) {
this.sessionManager = sessionManager;
}
public void setMediaChangeListener(MediaChange listener) {
this.mediaChangeListener = listener;
}
public void start() {
if (started) {
return;
}
sessionManager.addOnActiveSessionsChangedListener(new MediaSessionManager.OnActiveSessionsChangedListener() {
@Override
public void onActiveSessionsChanged(List<MediaController> controllers) {
Ln.i("MediaManager: Active Sessions changed");
if (controllers == null) {
controllers = Collections.emptyList();
}
// add
for(MediaController controller : controllers) {
if (!mediaControllers.contains(controller)) {
addMediaController(controller);
}
}
for(MediaController controller: mediaControllers) {
if (!controllers.contains(controller)) {
removeMediaController(controller);
}
}
mediaControllers = new ArrayList<>(controllers);
}
}, null);
mediaControllers = sessionManager.getActiveSessions(null);
for (MediaController controller : mediaControllers) {
addMediaController(controller);
}
started = true;
}
public static byte[] mediaMetadataSerialize(MediaMetadata metadata) {
ArrayList<Byte> payload = new ArrayList<Byte>();
for (String key : metadata.keySet()) {
byte field_id;
byte[] field_value;
switch(key) {
case MediaMetadata.METADATA_KEY_DURATION:
field_id = KEY_DURATION;
field_value = (""+metadata.getLong(key)).getBytes();
break;
case MediaMetadata.METADATA_KEY_ALBUM:
field_id = KEY_ALBUM;
field_value = metadata.getString(key).getBytes();
break;
case MediaMetadata.METADATA_KEY_ARTIST:
field_id = KEY_ARTIST;
field_value = metadata.getString(key).getBytes();
break;
case MediaMetadata.METADATA_KEY_TITLE:
field_id = KEY_TITLE;
field_value = metadata.getString(key).getBytes();
break;
default:
field_id = 0;
field_value = null;
}
if (field_value != null) {
payload.add(field_id);
for (byte b : field_value) {
payload.add(b);
}
payload.add((byte)0);
}
}
byte[] result = new byte[payload.size()];
for (int i = 0; i < payload.size(); i++) {
result[i] = payload.get(i);
}
return result;
}
public int playbackStateSerialize(PlaybackState state) {
switch(state.getState()) {
case PlaybackState.STATE_PLAYING:
return STATE_PLAYING;
case PlaybackState.STATE_STOPPED:
return STATE_STOPPED;
case PlaybackState.STATE_PAUSED:
return STATE_PAUSED;
default:
return -1;
}
}
private void removeMediaController(MediaController controller) {
int controllerId = findId(controller);
Ln.i("Remove MediaController ID:" + controllerId + " pkg:" + controller.getPackageName());
mediaChangeListener.onRemove(controllerId);
}
private int findId(MediaController controller) {
String packageName = controller.getPackageName();
Integer id = this.idMap.get(packageName);
if (id == null) {
id = nextId;
nextId++;
this.idMap.put(packageName, id);
}
return id;
}
private void addMediaController(MediaController controller) {
final int controllerId = findId(controller);
Ln.i("New MediaController ID:" + controllerId + " pkg:" + controller.getPackageName());
controller.registerCallback(new MediaController.Callback() {
@Override
public void onMetadataChanged(MediaMetadata metadata) {
super.onMetadataChanged(metadata);
Ln.i("MediaController metadata change " + controllerId);
mediaChangeListener.onMetadataChange(controllerId, metadata);
}
@Override
public void onPlaybackStateChanged(PlaybackState state) {
super.onPlaybackStateChanged(state);
Ln.i("MediaController playstate change " + controllerId);
mediaChangeListener.onPlaybackStateChange(controllerId, state);
}
});
mediaChangeListener.onMetadataChange(controllerId, controller.getMetadata());
mediaChangeListener.onPlaybackStateChange(controllerId, controller.getPlaybackState());
}
}

View file

@ -32,6 +32,7 @@ public final class ServiceManager {
private static ClipboardManager clipboardManager;
private static ActivityManager activityManager;
private static CameraManager cameraManager;
private static MediaManager mediaManager;
private ServiceManager() {
/* not instantiable */
@ -109,4 +110,11 @@ public final class ServiceManager {
}
return cameraManager;
}
public static MediaManager getMediaManager() {
if (mediaManager == null) {
mediaManager = MediaManager.create();
}
return mediaManager;
}
}