mirror of
https://github.com/pikvm/ustreamer.git
synced 2026-02-19 16:26:30 +00:00
Compare commits
20 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bfa1516491 | ||
|
|
36c9ff22b3 | ||
|
|
17f54a7977 | ||
|
|
574986d0fd | ||
|
|
d5ce2e835f | ||
|
|
6201554ba1 | ||
|
|
75dee4e91d | ||
|
|
e1b4e0db66 | ||
|
|
0d37b09bb4 | ||
|
|
18767b68ff | ||
|
|
7975615c6c | ||
|
|
90f09b197e | ||
|
|
687e97d523 | ||
|
|
c1675001fa | ||
|
|
69cc45a2a0 | ||
|
|
ece96b5834 | ||
|
|
383ed7530b | ||
|
|
7f620c758f | ||
|
|
fb50eea526 | ||
|
|
63f757a9da |
@@ -1,7 +1,7 @@
|
||||
[bumpversion]
|
||||
commit = True
|
||||
tag = True
|
||||
current_version = 5.20
|
||||
current_version = 5.24
|
||||
parse = (?P<major>\d+)\.(?P<minor>\d+)
|
||||
serialize =
|
||||
{major}.{minor}
|
||||
|
||||
@@ -7,3 +7,4 @@
|
||||
!python/**
|
||||
!janus/**
|
||||
!man/**
|
||||
!pkg/docker/entry.sh
|
||||
|
||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,4 +1,4 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
patreon: pikvm
|
||||
#custom: https://www.paypal.me/mdevaev
|
||||
custom: https://paypal.me/pikvm
|
||||
|
||||
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
81
.github/workflows/docker-alpine-image.yaml
vendored
Normal file
@@ -0,0 +1,81 @@
|
||||
name: Build Alpine
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, build_*]
|
||||
tags: [v*]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
buildx:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Login to GitHub Container Registry
|
||||
if: github.event_name != 'pull_request'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
-
|
||||
name: Login to DockerHub Container Registry
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@v4
|
||||
with:
|
||||
images: |
|
||||
${{ secrets.DOCKERHUB_USERNAME }}/ustreamer,enable=${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||
ghcr.io/${{ github.repository }},enable=${{ github.event_name != 'pull_request' }}
|
||||
tags: |
|
||||
type=ref,event=tag
|
||||
type=sha,format=long
|
||||
-
|
||||
name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v2
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
with:
|
||||
driver-opts: image=moby/buildkit:master
|
||||
-
|
||||
name: Build and export to Docker
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
load: true
|
||||
tags: ustreamer
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Test
|
||||
run: |
|
||||
echo version: $(docker run --rm -e NO_EDID=1 -t ustreamer --version)
|
||||
echo -e "features:\n$(docker run --rm -e NO_EDID=1 -t ustreamer --features)"
|
||||
-
|
||||
name: Build multi arch
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
-
|
||||
name: Push
|
||||
if: steps.meta.outputs.tags != ''
|
||||
uses: docker/build-push-action@v3
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/arm64,linux/amd64,linux/arm/v7
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
file: pkg/docker/Dockerfile.alpine
|
||||
29
README.md
29
README.md
@@ -5,7 +5,7 @@
|
||||
[[Русская версия]](README.ru.md)
|
||||
|
||||
µStreamer is a lightweight and very quick server to stream [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) video from any V4L2 device to the net. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc.
|
||||
µStreamer is a part of the [Pi-KVM](https://github.com/pikvm/pikvm) project designed to stream [VGA](https://www.amazon.com/dp/B0126O0RDC) and [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) screencast hardware data with the highest resolution and FPS possible.
|
||||
µStreamer is a part of the [PiKVM](https://github.com/pikvm/pikvm) project designed to stream [VGA](https://www.amazon.com/dp/B0126O0RDC) and [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) screencast hardware data with the highest resolution and FPS possible.
|
||||
|
||||
µStreamer is very similar to [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) with ```input_uvc.so``` and ```output_http.so``` plugins, however, there are some major differences. The key ones are:
|
||||
|
||||
@@ -81,6 +81,33 @@ $ ./ustreamer \
|
||||
|
||||
You can always view the full list of options with ```ustreamer --help```.
|
||||
|
||||
# Docker (Raspberry Pi 4 HDMI)
|
||||
## Preparations
|
||||
|
||||
Add following lines to /boot/firmware/usercfg.txt:
|
||||
```
|
||||
gpu_mem=128
|
||||
dtoverlay=tc358743
|
||||
```
|
||||
Check size of CMA:
|
||||
```bash
|
||||
$ dmesg | grep cma-reserved
|
||||
[ 0.000000] Memory: 7700524K/8244224K available (11772K kernel code, 1278K rwdata, 4320K rodata, 4096K init, 1077K bss, 281556K reserved, 262144K cma-reserved)
|
||||
```
|
||||
If it is smaller than 128M add following to /boot/firmware/cmdline.txt:
|
||||
```
|
||||
cma=128M
|
||||
```
|
||||
Save changes and reboot.
|
||||
## Launch
|
||||
Start container:
|
||||
```bash
|
||||
$ docker run --device /dev/video0:/dev/video0 -p 8080:8080 pikvm/ustreamer:latest
|
||||
```
|
||||
Then access the web interface at port 8080 (e.g. http://raspberrypi.local:8080).
|
||||
## EDID
|
||||
Container will set HDMI EDID before starging ustreamer. Use `-e NO_EDID=1` to not set EDID. Use `-e EDID_HEX=xx` to specify custom EDID data.
|
||||
|
||||
-----
|
||||
# Raspberry Pi Camera Example
|
||||
Example usage for the Raspberry Pi v1 camera:
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[[English version]](README.md)
|
||||
|
||||
|
||||
µStreamer - это маленький и очень быстрый сервер, который позволяет организовать трансляцию видео в формате [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) с любого устройства V4L2 в сеть. Этот формат нативно поддерживается всеми современными браузерами и большинством приложений для просмотра видео (mplayer, VLC и так далее). µStreamer был разработан в рамках проекта [Pi-KVM](https://github.com/pikvm/pikvm) специально для стриминга с устройств видеозахвата [VGA](https://www.amazon.com/dp/B0126O0RDC) и [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) с максимально возможным разрешением и FPS, которые только позволяет железо.
|
||||
µStreamer - это маленький и очень быстрый сервер, который позволяет организовать трансляцию видео в формате [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) с любого устройства V4L2 в сеть. Этот формат нативно поддерживается всеми современными браузерами и большинством приложений для просмотра видео (mplayer, VLC и так далее). µStreamer был разработан в рамках проекта [PiKVM](https://github.com/pikvm/pikvm) специально для стриминга с устройств видеозахвата [VGA](https://www.amazon.com/dp/B0126O0RDC) и [HDMI](https://auvidea.com/b101-hdmi-to-csi-2-bridge-15-pin-fpc/) с максимально возможным разрешением и FPS, которые только позволяет железо.
|
||||
|
||||
Функционально µStreamer очень похож на [mjpg-streamer](https://github.com/jacksonliam/mjpg-streamer) при использовании им плагинов ```input_uvc.so``` и ```output_http.so```, однако имеет ряд серьезных отличий. Основные приведены в этой таблице:
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ static int _plugin_init(janus_callbacks *gw, const char *config_dir_path) {
|
||||
// sysctl -w net.core.rmem_max=1000000
|
||||
// sysctl -w net.core.wmem_max=1000000
|
||||
|
||||
US_JLOG_INFO("main", "Initializing plugin ...");
|
||||
US_JLOG_INFO("main", "Initializing PiKVM uStreamer plugin %s ...", US_VERSION);
|
||||
if (gw == NULL || config_dir_path == NULL || ((_g_config = us_config_init(config_dir_path)) == NULL)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ FROM archlinux/archlinux:base-devel
|
||||
RUN mkdir -p /etc/pacman.d/hooks \
|
||||
&& ln -s /dev/null /etc/pacman.d/hooks/30-systemd-tmpfiles.hook
|
||||
|
||||
RUN echo "Server = http://mirror.yandex.ru/archlinux/\$repo/os/\$arch" > /etc/pacman.d/mirrorlist
|
||||
RUN echo 'Server = https://mirror.rackspace.com/archlinux/$repo/os/$arch' > /etc/pacman.d/mirrorlist
|
||||
|
||||
RUN pacman -Syu --noconfirm \
|
||||
&& pacman -S --needed --noconfirm \
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Manpage for ustreamer-dump.
|
||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||
.TH USTREAMER-DUMP 1 "version 5.20" "January 2021"
|
||||
.TH USTREAMER-DUMP 1 "version 5.24" "January 2021"
|
||||
|
||||
.SH NAME
|
||||
ustreamer-dump \- Dump uStreamer's memory sink to file
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Manpage for ustreamer.
|
||||
.\" Open an issue or pull request to https://github.com/pikvm/ustreamer to correct errors or typos
|
||||
.TH USTREAMER 1 "version 5.20" "November 2020"
|
||||
.TH USTREAMER 1 "version 5.24" "November 2020"
|
||||
|
||||
.SH NAME
|
||||
ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||
@@ -10,7 +10,7 @@ ustreamer \- stream MJPEG video from any V4L2 device to the network
|
||||
.RI [OPTIONS]
|
||||
|
||||
.SH DESCRIPTION
|
||||
µStreamer (\fBustreamer\fP) is a lightweight and very quick server to stream MJPEG video from any V4L2 device to the network. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc. µStreamer is a part of the Pi-KVM project designed to stream VGA and HDMI screencast hardware data with the highest resolution and FPS possible.
|
||||
µStreamer (\fBustreamer\fP) is a lightweight and very quick server to stream MJPEG video from any V4L2 device to the network. All new browsers have native support of this video format, as well as most video players such as mplayer, VLC etc. µStreamer is a part of the PiKVM project designed to stream VGA and HDMI screencast hardware data with the highest resolution and FPS possible.
|
||||
|
||||
.SH USAGE
|
||||
Without arguments, \fBustreamer\fR will try to open \fB/dev/video0\fR with 640x480 resolution and start streaming on \fBhttp://127\.0\.0\.1:8080\fR\. You can override this behavior using parameters \fB\-\-device\fR, \fB\-\-host\fR and \fB\-\-port\fR\. For example, to stream to the world, run: \fBustreamer --device=/dev/video1 --host=0.0.0.0 --port=80\fR
|
||||
@@ -203,6 +203,9 @@ Default: disabled.
|
||||
.BR \-\-allow\-origin\ \fIstr
|
||||
Set Access\-Control\-Allow\-Origin header. Default: disabled.
|
||||
.TP
|
||||
.BR \-\-instance\-id\ \fIstr
|
||||
A short string identifier to be displayed in the /state handle. It must satisfy regexp ^[a-zA-Z0-9\./+_-]*$. Default: an empty string.
|
||||
.TP
|
||||
.BR \-\-server\-timeout\ \fIsec
|
||||
Timeout for client connections. Default: 10.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
|
||||
pkgname=ustreamer
|
||||
pkgver=5.20
|
||||
pkgver=5.24
|
||||
pkgrel=1
|
||||
pkgdesc="Lightweight and fast MJPEG-HTTP streamer"
|
||||
url="https://github.com/pikvm/ustreamer"
|
||||
|
||||
34
pkg/docker/Dockerfile.alpine
Normal file
34
pkg/docker/Dockerfile.alpine
Normal file
@@ -0,0 +1,34 @@
|
||||
FROM alpine:3.16 as build
|
||||
RUN apk add --no-cache \
|
||||
alpine-sdk \
|
||||
linux-headers \
|
||||
libjpeg-turbo-dev \
|
||||
libevent-dev \
|
||||
libbsd-dev \
|
||||
libgpiod-dev
|
||||
|
||||
WORKDIR /build/ustreamer/
|
||||
COPY . .
|
||||
RUN make -j5 WITH_GPIO=1
|
||||
|
||||
FROM alpine:3.16 as run
|
||||
|
||||
RUN apk add --no-cache \
|
||||
libevent \
|
||||
libjpeg-turbo \
|
||||
libevent \
|
||||
libgpiod \
|
||||
libbsd \
|
||||
v4l-utils
|
||||
|
||||
WORKDIR /ustreamer
|
||||
COPY --from=build /build/ustreamer/src/ustreamer.bin ustreamer
|
||||
|
||||
RUN wget https://raw.githubusercontent.com/pikvm/kvmd/master/configs/kvmd/tc358743-edid.hex -O /edid.hex
|
||||
COPY pkg/docker/entry.sh /
|
||||
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["/entry.sh"]
|
||||
CMD ["--dv-timings", "--format", "UYVY"]
|
||||
|
||||
# vim: syntax=dockerfile
|
||||
14
pkg/docker/entry.sh
Executable file
14
pkg/docker/entry.sh
Executable file
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
[ -z "$NO_EDID" ] && {
|
||||
[ -n "$EDID_HEX" ] && echo "$EDID_HEX" > /edid.hex
|
||||
while true; do
|
||||
v4l2-ctl --device=/dev/video0 --set-edid=file=/edid.hex --fix-edid-checksums --info-edid && break
|
||||
echo 'Failed to set EDID. Reetrying...'
|
||||
sleep 1
|
||||
done
|
||||
}
|
||||
|
||||
./ustreamer --host=0.0.0.0 $@
|
||||
@@ -6,7 +6,7 @@
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=ustreamer
|
||||
PKG_VERSION:=5.20
|
||||
PKG_VERSION:=5.24
|
||||
PKG_RELEASE:=1
|
||||
PKG_MAINTAINER:=Maxim Devaev <mdevaev@gmail.com>
|
||||
|
||||
|
||||
@@ -1,14 +1,12 @@
|
||||
import os
|
||||
|
||||
from typing import List
|
||||
|
||||
from setuptools import Extension
|
||||
from setuptools import setup
|
||||
|
||||
|
||||
# =====
|
||||
def _find_sources(suffix: str) -> List[str]:
|
||||
sources: List[str] = []
|
||||
def _find_sources(suffix: str) -> list[str]:
|
||||
sources: list[str] = []
|
||||
for (root_path, _, names) in os.walk("src"):
|
||||
for name in names:
|
||||
if name.endswith(suffix):
|
||||
@@ -19,7 +17,7 @@ def _find_sources(suffix: str) -> List[str]:
|
||||
if __name__ == "__main__":
|
||||
setup(
|
||||
name="ustreamer",
|
||||
version="5.20",
|
||||
version="5.24",
|
||||
description="uStreamer tools",
|
||||
author="Maxim Devaev",
|
||||
author_email="mdevaev@gmail.com",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#pragma once
|
||||
|
||||
#define US_VERSION_MAJOR 5
|
||||
#define US_VERSION_MINOR 20
|
||||
#define US_VERSION_MINOR 24
|
||||
|
||||
#define US_MAKE_VERSION2(_major, _minor) #_major "." #_minor
|
||||
#define US_MAKE_VERSION1(_major, _minor) US_MAKE_VERSION2(_major, _minor)
|
||||
|
||||
@@ -75,6 +75,7 @@ us_server_s *us_server_init(us_stream_s *stream) {
|
||||
server->passwd = "";
|
||||
server->static_path = "";
|
||||
server->allow_origin = "";
|
||||
server->instance_id = "";
|
||||
server->timeout = 10;
|
||||
server->run = run;
|
||||
|
||||
@@ -400,7 +401,9 @@ static void _http_callback_state(struct evhttp_request *request, void *v_server)
|
||||
|
||||
_A_EVBUFFER_ADD_PRINTF(buf,
|
||||
"{\"ok\": true, \"result\": {"
|
||||
" \"instance_id\": \"%s\","
|
||||
" \"encoder\": {\"type\": \"%s\", \"quality\": %u},",
|
||||
server->instance_id,
|
||||
us_encoder_type_to_string(enc_type),
|
||||
enc_quality
|
||||
);
|
||||
|
||||
@@ -149,6 +149,7 @@ typedef struct us_server_sx {
|
||||
char *passwd;
|
||||
char *static_path;
|
||||
char *allow_origin;
|
||||
char *instance_id;
|
||||
|
||||
unsigned drop_same_frames;
|
||||
unsigned fake_width;
|
||||
|
||||
@@ -82,6 +82,7 @@ enum _US_OPT_VALUES {
|
||||
_O_PASSWD,
|
||||
_O_STATIC,
|
||||
_O_ALLOW_ORIGIN,
|
||||
_O_INSTANCE_ID,
|
||||
_O_TCP_NODELAY,
|
||||
_O_SERVER_TIMEOUT,
|
||||
|
||||
@@ -177,6 +178,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
{"static", required_argument, NULL, _O_STATIC},
|
||||
{"drop-same-frames", required_argument, NULL, _O_DROP_SAME_FRAMES},
|
||||
{"allow-origin", required_argument, NULL, _O_ALLOW_ORIGIN},
|
||||
{"instance-id", required_argument, NULL, _O_INSTANCE_ID},
|
||||
{"fake-resolution", required_argument, NULL, _O_FAKE_RESOLUTION},
|
||||
{"tcp-nodelay", no_argument, NULL, _O_TCP_NODELAY},
|
||||
{"server-timeout", required_argument, NULL, _O_SERVER_TIMEOUT},
|
||||
@@ -228,6 +230,7 @@ static const struct option _LONG_OPTS[] = {
|
||||
|
||||
|
||||
static int _parse_resolution(const char *str, unsigned *width, unsigned *height, bool limited);
|
||||
static int _check_instance_id(const char *str);
|
||||
|
||||
static void _features(void);
|
||||
static void _help(FILE *fp, us_device_s *dev, us_encoder_s *enc, us_stream_s *stream, us_server_s *server);
|
||||
@@ -419,6 +422,13 @@ int options_parse(us_options_s *options, us_device_s *dev, us_encoder_s *enc, us
|
||||
case _O_DROP_SAME_FRAMES: OPT_NUMBER("--drop-same-frames", server->drop_same_frames, 0, US_VIDEO_MAX_FPS, 0);
|
||||
case _O_FAKE_RESOLUTION: OPT_RESOLUTION("--fake-resolution", server->fake_width, server->fake_height, false);
|
||||
case _O_ALLOW_ORIGIN: OPT_SET(server->allow_origin, optarg);
|
||||
case _O_INSTANCE_ID:
|
||||
if (_check_instance_id(optarg) != 0) {
|
||||
printf("Invalid instance ID, it should be like: ^[a-zA-Z0-9\\./+_-]*$\n");
|
||||
return -1;
|
||||
}
|
||||
server->instance_id = optarg;
|
||||
break;
|
||||
case _O_TCP_NODELAY: OPT_SET(server->tcp_nodelay, true);
|
||||
case _O_SERVER_TIMEOUT: OPT_NUMBER("--server-timeout", server->timeout, 1, 60, 0);
|
||||
|
||||
@@ -473,6 +483,8 @@ int options_parse(us_options_s *options, us_device_s *dev, us_encoder_s *enc, us
|
||||
}
|
||||
}
|
||||
|
||||
US_LOG_INFO("Starting PiKVM uStreamer %s ...", US_VERSION);
|
||||
|
||||
options->blank = us_blank_frame_init(blank_path);
|
||||
stream->blank = options->blank;
|
||||
|
||||
@@ -530,6 +542,18 @@ static int _parse_resolution(const char *str, unsigned *width, unsigned *height,
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int _check_instance_id(const char *str) {
|
||||
for (const char *ptr = str; *ptr; ++ptr) {
|
||||
if (!(isascii(*ptr) && (
|
||||
isalpha(*ptr) || isdigit(*ptr)
|
||||
|| *ptr == '.' || *ptr == '/' || *ptr == '+' || *ptr == '_' || *ptr == '-'
|
||||
))) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void _features(void) {
|
||||
# ifdef WITH_GPIO
|
||||
puts("+ WITH_GPIO");
|
||||
@@ -657,6 +681,8 @@ static void _help(FILE *fp, us_device_s *dev, us_encoder_s *enc, us_stream_s *st
|
||||
SAY(" --tcp-nodelay ────────────── Set TCP_NODELAY flag to the client /stream socket. Only for TCP socket.");
|
||||
SAY(" Default: disabled.\n");
|
||||
SAY(" --allow-origin <str> ─────── Set Access-Control-Allow-Origin header. Default: disabled.\n");
|
||||
SAY(" --instance-id <str> ──────── A short string identifier to be displayed in the /state handle.");
|
||||
SAY(" It must satisfy regexp ^[a-zA-Z0-9\\./+_-]*$. Default: an empty string.\n");
|
||||
SAY(" --server-timeout <sec> ───── Timeout for client connections. Default: %u.\n", server->timeout);
|
||||
# define ADD_SINK(x_name, x_opt) \
|
||||
SAY(x_name " sink options:"); \
|
||||
|
||||
@@ -26,13 +26,11 @@ import os
|
||||
import io
|
||||
import struct
|
||||
|
||||
from typing import Tuple
|
||||
|
||||
import common
|
||||
|
||||
|
||||
# =====
|
||||
def _get_jpeg_size(data: bytes) -> Tuple[int, int]:
|
||||
def _get_jpeg_size(data: bytes) -> tuple[int, int]:
|
||||
# https://sheep.horse/2013/9/finding_the_dimensions_of_a_jpeg_file_in_python.html
|
||||
|
||||
stream = io.BytesIO(data)
|
||||
|
||||
Reference in New Issue
Block a user