diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1ae8cae43c..da19726040 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,8 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + SC_CONTROL_MSG_TYPE_MEDIA_STATE, + SC_CONTROL_MSG_TYPE_MEDIA_SEEK, }; enum sc_screen_power_mode { @@ -110,6 +112,14 @@ struct sc_control_msg { struct { uint16_t id; } uhid_destroy; + struct { + uint16_t player_id; + uint64_t position; + } media_seek; + struct { + uint16_t player_id; + uint8_t state; + } media_state; }; }; diff --git a/app/src/device_msg.c b/app/src/device_msg.c index 7621c040ec..06b42ddc98 100644 --- a/app/src/device_msg.c +++ b/app/src/device_msg.c @@ -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,21 +63,43 @@ 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(); + + msg->uhid_output.id = id; + msg->uhid_output.size = size; + if (read_message(&msg->uhid_output.data, &buf[5], size) == -1) { return -1; } - if (size) { - memcpy(data, &buf[5], size); + + 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->uhid_output.id = id; - msg->uhid_output.size = size; - msg->uhid_output.data = data; + 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); return -1; // error, we cannot recover @@ -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; diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 86b2ccb7cc..e241bcb136 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -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; }; }; diff --git a/app/src/receiver.c b/app/src/receiver.c index b89b0c6e18..e2771e68c8 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -74,6 +74,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) { @@ -150,6 +166,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; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 51daeced16..efebd77bed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -21,6 +21,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; @@ -81,6 +82,10 @@ public boolean getAudio() { return audio; } + public boolean getMediaControls() { + return mediaControl; + } + public int getMaxSize() { return maxSize; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7817fdf5b9..c86ab00156 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -23,7 +23,10 @@ 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.media.MediaMetadata; +import android.media.session.PlaybackState; import android.os.BatteryManager; import android.os.Build; @@ -139,6 +142,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc boolean control = options.getControl(); boolean video = options.getVideo(); boolean audio = options.getAudio(); + boolean media = options.getMediaControls(); boolean sendDummyByte = options.getSendDummyByte(); boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; @@ -162,6 +166,40 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc controller.getSender().send(msg); }); 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) { @@ -200,6 +238,9 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc asyncProcessors.add(surfaceEncoder); } + + + Completion completion = new Completion(asyncProcessors.size()); for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.start((fatalError) -> { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index d1406ed0a1..798c17f33a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -23,6 +23,8 @@ public final class ControlMessage { public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; + public static final int TYPE_MEDIA_STATE = 16; + public static final int TYPE_MEDIA_SEEK = 17; public static final long SEQUENCE_INVALID = 0; @@ -48,6 +50,9 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; + private int mediaState; + private long mediaSeek; + private ControlMessage() { } @@ -155,6 +160,22 @@ public static ControlMessage createUhidDestroy(int id) { return msg; } + 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; + return msg; + } + public int getType() { return type; } @@ -226,4 +247,12 @@ public int getId() { public byte[] getData() { return data; } + + public long getMediaSeek() { + return mediaSeek; + } + + public int getMediaState() { + return mediaState; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 45116935f7..f8844eacf2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -53,11 +53,27 @@ public ControlMessage read() throws IOException { return parseUhidInput(); case ControlMessage.TYPE_UHID_DESTROY: return parseUhidDestroy(); + 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(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 3825165522..eea55d68f7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -221,6 +221,12 @@ private boolean handleEvent() throws IOException { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; + 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 } @@ -228,6 +234,14 @@ private boolean handleEvent() throws IOException { 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 (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { schedulePowerModeOff(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java index 079a7a04fb..8994a788c1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessage.java @@ -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 static DeviceMessage createUhidOutput(int id, byte[] data) { 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; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java index a18a2e5dcd..6b9ebe7ed4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/DeviceMessageWriter.java @@ -22,6 +22,7 @@ public DeviceMessageWriter(OutputStream rawOutputStream) { 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 void write(DeviceMessage msg) throws IOException { 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); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java new file mode 100644 index 0000000000..2efee1f4be --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/MediaManager.java @@ -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 idMap = new HashMap<>(); + List 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 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 payload = new ArrayList(); + 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()); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dabb5..a50eaa148b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -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 */ @@ -108,4 +109,11 @@ public static CameraManager getCameraManager() { } return cameraManager; } + + public static MediaManager getMediaManager() { + if (mediaManager == null) { + mediaManager = MediaManager.create(); + } + return mediaManager; + } }