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

Support reMarkable 2 #31

Merged
merged 16 commits into from
Jan 1, 2021
Merged
14 changes: 14 additions & 0 deletions .cargo/config
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[target.armv7-unknown-linux-gnueabihf]
linker = "/usr/local/oecore-x86_64/sysroots/x86_64-oesdk-linux/usr/bin/arm-oe-linux-gnueabi/arm-oe-linux-gnueabi-gcc"
rustflags = [
"-C", "link-arg=-march=armv7-a",
"-C", "link-arg=-marm",
"-C", "link-arg=-mfpu=neon",
"-C", "link-arg=-mfloat-abi=hard",
"-C", "link-arg=-mcpu=cortex-a9",
"-C", "link-arg=--sysroot=/usr/local/oecore-x86_64/sysroots/cortexa9hf-neon-oe-linux-gnueabi",
]

[build]
# Set the default --target flag
target = "armv7-unknown-linux-gnueabihf"
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
9 changes: 9 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[package]
name = "restream"
version = "0.1.0"
authors = ["Rien Maertens <rien.maertens@posteo.be>"]
edition = "2018"

[dependencies]
anyhow = "1.0"
lz-fear = "0.1"
35 changes: 14 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,21 @@
reMarkable screen sharing over SSH.

[![rm1](https://img.shields.io/badge/rM1-supported-green)](https://remarkable.com/store/remarkable)
[![rm2](https://img.shields.io/badge/rM2-unsupported-red)](https://remarkable.com/store/remarkable-2)
[![rm2](https://img.shields.io/badge/rM2-supported-green)](https://remarkable.com/store/remarkable-2)

![A demo of reStream](extra/demo.gif)

## Installation

1. Clone this repository: `git clone https://github.com/rien/reStream`.
2. (Optional but recommended) [Install lz4 on your host and reMarkable](#sub-second-latency).
2. Install `lz4` on your host with your usual package manager. On Ubuntu,
`apt install liblz4-tool` will do the trick.
3. [Set up an SSH key and add it to the ssh-agent](https://help.github.com/en/github/authenticating-to-github/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent), then add your key to the reMarkable with `ssh-copy-id root@10.11.99.1`.
4. Copy the `restream` executable to the reMarkable and make it executable.
```
# scp restream.arm.static root@10.11.99.1:/home/root/restream
# ssh root@10.11.99.1 'chmod +x /home/root/restream'
```

## Usage

Expand All @@ -27,7 +33,7 @@ reMarkable screen sharing over SSH.
- `-s --source`: the ssh destination of the reMarkable (default: `root@10.11.99.1`)
- `-o --output`: path of the output where the video should be recorded, as understood by `ffmpeg`; if this is `-`, the video is displayed in a new window and not recorded anywhere (default: `-`)
- `-f --format`: when recording to an output, this option is used to force the encoding format; if this is `-`, `ffmpeg`’s auto format detection based on the file extension is used (default: `-`).
- `-w --webcam`: record to a video4linux2 web cam device. By default the first found web cam is taken, this can be overwritten with `-o`. The video is scaled to 1280x720 to ensure compatibility with MS Teams, Skype for business and other programs which need this specific format.
- `-w --webcam`: record to a video4linux2 web cam device. By default the first found web cam is taken, this can be overwritten with `-o`. The video is scaled to 1280x720 to ensure compatibility with MS Teams, Skype for business and other programs which need this specific format. See [video4linux loopback](#video4linux-loopback) for installation instructions.
- `-m --measure`: use `pv` to measure how much data throughput you have (good to experiment with parameters to speed up the pipeline)
- `-t --title`: set a custom window title for the video stream. The default title is "reStream". This option is disabled when using `-o --output`

Expand All @@ -41,24 +47,7 @@ On your **host** machine:
- ssh
- Video4Linux loopback kernel module if you want to use `--webcam`

On your **reMarkable** nothing is needed, unless you want...

### Sub-second latency

To achieve sub-second latency, you'll need [lz4](https://github.com/lz4/lz4)
on your host and on your reMarkable.

You can install `lz4` on your host with your usual package manager. On Ubuntu,
`apt install liblz4-tool` will do the trick.

On your **reMarkable** you'll need a binary of `lz4` build for the arm platform,
you can do this yourself by [installing the reMarkable toolchain](https://remarkablewiki.com/devel/qt_creator#toolchain)
and compiling `lz4` from source with the toolchain enabled, or you can use the
statically linked binary I have already built and put in this repo.

Copy the `lz4` program to your reMarkable with
`scp lz4.arm.static root@10.11.99.1:/home/root/lz4`, make it executable with
`ssh root@10.11.99.1 'chmod +x /home/root/lz4'` and you're ready to go.
On your **reMarkable** you need the `restream` binary (see [installation instructions](#installation)).

### Video4Linux Loopback

Expand Down Expand Up @@ -89,3 +78,7 @@ The result should contain a line with "platform:v4l2loopback".
Steps you can try if the script isn't working:
- [Set up an SSH key](#installation)
- Update `ffmpeg` to version 4.

## Development

If you want to play with the `restream` code, you will have to [install Rust](https://www.rust-lang.org/learn/get-started) and [setup the reMarkable toolchain](https://github.com/canselcik/libremarkable#setting-up-the-toolchain) to do cross-platform development.
Binary file removed lz4.arm.static
Binary file not shown.
84 changes: 41 additions & 43 deletions reStream.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ format=- # automatic output format
webcam=false # not to a webcam
measure_throughput=false # measure how fast data is being transferred
window_title=reStream # stream window title is reStream
video_filters="" # list of ffmpeg filters to apply

# loop through arguments and process them
while [ $# -gt 0 ]; do
Expand Down Expand Up @@ -75,14 +76,8 @@ while [ $# -gt 0 ]; do
esac
done

# technical parameters
width=1408

Choose a reason for hiding this comment

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

  • width, height are switched on the RM2.
  • bytes per pixel is 1 on the RM2
  • i'm wondering if loop_wait might benefit from having a small sleep? Still yet to experiment

Copy link
Owner Author

Choose a reason for hiding this comment

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

I have experimented with sleeps, but it ended up not being beneficial I think. Anyway, we should speed up reading the framebuffer before changing loop_wait.

height=1872
bytes_per_pixel=2
loop_wait="true"
loglevel="info"

ssh_cmd() {
echo "[SSH]" "$@" >&2
ssh -o ConnectTimeout=1 "$ssh_host" "$@"
}

Expand All @@ -92,30 +87,45 @@ if ! ssh_cmd true; then
exit 1
fi

fallback_to_gzip() {
echo "Falling back to gzip, your experience may not be optimal."
echo "Go to https://github.com/rien/reStream/#sub-second-latency for a better experience."
compress="gzip"
decompress="gzip -d"
sleep 2
}
rm_version="$(ssh_cmd cat /sys/devices/soc0/machine)"

case "$rm_version" in
"reMarkable 1.0")
width=1408
height=1872
pixel_format="rgb565le"
;;
"reMarkable 2.0")
pixel_format="gray8"
width=1872
height=1404
video_filters="$video_filters,transpose=2"
;;
*)
echo "Unsupported reMarkable version: $rm_version."
echo "Please visit https://github.com/rien/reStream/ for updates."
exit 1
;;
esac

# check if lz4 is present on remarkable
if ssh_cmd "[ -f /opt/bin/lz4 ]"; then
compress="/opt/bin/lz4"
elif ssh_cmd "[ -f ~/lz4 ]"; then
compress="\$HOME/lz4"
fi
# technical parameters
loglevel="info"
decompress="lz4 -d"

# gracefully degrade to gzip if is not present on remarkable or host
if [ -z "$compress" ]; then
echo "Your remarkable does not have lz4."
fallback_to_gzip
elif ! lz4 -V >/dev/null; then
# check if lz4 is present on the host
if ! lz4 -V >/dev/null; then
echo "Your host does not have lz4."
fallback_to_gzip
else
decompress="lz4 -d"
echo "Please install it using the instruction in the README:"
echo "https://github.com/rien/reStream/#installation"
exit 1
fi

# check if restream binay is present on remarkable
if ssh_cmd "[ ! -f ~/restream ]"; then
echo "The restream binary is not installed on your reMarkable."
echo "Please install it using the instruction in the README:"
echo "https://github.com/rien/reStream/#installation"
exit 1
fi

# use pv to measure throughput if desired, else we just pipe through cat
Expand All @@ -131,21 +141,15 @@ else
host_passthrough="cat"
fi

# list of ffmpeg filters to apply
video_filters=""

# store extra ffmpeg arguments in $@
set --

# calculate how much bytes the window is
window_bytes="$((width * height * bytes_per_pixel))"

# rotate 90 degrees if landscape=true
$landscape && video_filters="$video_filters,transpose=1"
Copy link

@harrylepotter harrylepotter Oct 29, 2020

Choose a reason for hiding this comment

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

i think transpose might=2 on the RM2

Copy link
Owner Author

Choose a reason for hiding this comment

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

Can you confirm this? transpose=2 will flip it completely upside down?

Copy link

@gcotone gcotone Nov 3, 2020

Choose a reason for hiding this comment

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

@harrylepotter indeed, I'm on 2.3.1.27 and this was required to make orientation work properly:

! $landscape && video_filters="$video_filters,transpose=2"


# Scale and add padding if we are targeting a webcam because a lot of services
# expect a size of exactly 1280x720 (tested in Firefox, MS Teams, and Skype for
# for business). Send a PR is you can get a heigher resolution working.
# for business). Send a PR if you can get a higher resolution working.
if $webcam; then
video_filters="$video_filters,format=pix_fmts=yuv420p"
video_filters="$video_filters,scale=-1:720"
Expand All @@ -155,12 +159,6 @@ fi
# set each frame presentation time to the time it is received
video_filters="$video_filters,setpts=(RTCTIME - RTCSTART) / (TB * 1000000)"

# read the first $window_bytes of the framebuffer
head_fb0="dd if=/dev/fb0 count=1 bs=$window_bytes 2>/dev/null"

# loop that keeps on reading and compressing, to be executed remotely
read_loop="while $head_fb0; do $loop_wait; done | $compress"

set -- "$@" -vf "${video_filters#,}"

if [ "$output_path" = - ]; then
Expand All @@ -180,14 +178,14 @@ fi
set -e # stop if an error occurs

# shellcheck disable=SC2086
ssh_cmd "$read_loop" \
ssh_cmd "./restream" \
| $decompress \
| $host_passthrough \
| "$output_cmd" \
-vcodec rawvideo \
-loglevel "$loglevel" \
-f rawvideo \
-pixel_format rgb565le \
-pixel_format "$pixel_format" \
-video_size "$width,$height" \
$window_title_option \
-i - \
Expand Down
Binary file added restream.arm.static
Binary file not shown.
135 changes: 135 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
#[macro_use]
extern crate anyhow;
extern crate lz_fear;

use anyhow::{Context, Result};
use lz_fear::CompressionSettings;

use std::default::Default;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Seek, SeekFrom};
use std::process::Command;

fn main() -> Result<()> {
let version = remarkable_version()?;
let streamer = if version == "reMarkable 1.0\n" {
let width = 1408;
let height = 1872;
let bytes_per_pixel = 2;
ReStreamer::init("/dev/fb0", 0, width, height, bytes_per_pixel)?
} else if version == "reMarkable 2.0\n" {
let width = 1404;
let height = 1872;
let bytes_per_pixel = 1;

let pid = xochitl_pid()?;
let offset = rm2_fb_offset(pid)?;
let mem = format!("/proc/{}/mem", pid);
ReStreamer::init(&mem, offset, width, height, bytes_per_pixel)?
} else {
Err(anyhow!(
"Unknown reMarkable version: {}\nPlease open a feature request to support your device.",
version
))?
};

let lz4: CompressionSettings = Default::default();
lz4.compress(streamer, std::io::stdout().lock())
.context("Error while compressing framebuffer stream")
}

fn remarkable_version() -> Result<String> {
let content = std::fs::read("/sys/devices/soc0/machine")
.context("Failed to read /sys/devices/soc0/machine")?;
Ok(String::from_utf8(content)?)
}

fn xochitl_pid() -> Result<usize> {
let output = Command::new("/bin/pidof")
.args(&["xochitl"])
.output()
.context("Failed to run `/bin/pidof xochitl`")?;
if output.status.success() {
let pid = &output.stdout;
let pid_str = std::str::from_utf8(pid)?.trim();
pid_str
.parse()
.with_context(|| format!("Failed to parse xochitl's pid: {}", pid_str))
} else {
Err(anyhow!(
"Could not find pid of xochitl, is xochitl running?"
))
}
}

fn rm2_fb_offset(pid: usize) -> Result<usize> {
let file = File::open(format!("/proc/{}/maps", &pid))?;
let line = BufReader::new(file)
.lines()
.skip_while(|line| matches!(line, Ok(l) if !l.ends_with("/dev/fb0")))
.skip(1)
.next()
.with_context(|| format!("No line containing /dev/fb0 in /proc/{}/maps file", pid))?
.with_context(|| format!("Error reading file /proc/{}/maps", pid))?;

let addr = line
.split("-")
.next()
.with_context(|| format!("Error parsing line in /proc/{}/maps", pid))?;

let address = usize::from_str_radix(addr, 16).context("Error parsing framebuffer address")?;
Ok(address + 8)
}

pub struct ReStreamer {
file: File,
start: u64,
cursor: usize,
size: usize,
}

impl ReStreamer {
pub fn init(
path: &str,
offset: usize,
width: usize,
height: usize,
bytes_per_pixel: usize,
) -> Result<ReStreamer> {
let start = offset as u64;
let size = width * height * bytes_per_pixel;
let cursor = 0;
let file = File::open(path)?;
let mut streamer = ReStreamer {
file,
start: start,
cursor,
size,
};
streamer.next_frame()?;
Ok(streamer)
}

pub fn next_frame(&mut self) -> std::io::Result<()> {
self.file.seek(SeekFrom::Start(self.start))?;
self.cursor = 0;
Ok(())
}
}

impl Read for ReStreamer {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let requested = buf.len();
let bytes_read = if self.cursor + requested < self.size {
self.file.read(buf)?
} else {
let rest = self.size - self.cursor;
self.file.read(&mut buf[0..rest])?
};
self.cursor += bytes_read;
if self.cursor == self.size {
self.next_frame()?;
}
Ok(bytes_read)
}
}