Skip to content

Commit

Permalink
require JSON file with key
Browse files Browse the repository at this point in the history
  • Loading branch information
scivision committed Nov 30, 2022
1 parent 2644805 commit 3d52536
Show file tree
Hide file tree
Showing 21 changed files with 81 additions and 168 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.mypy_cache/
.pytest_cache/
*.key
*.json

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
25 changes: 19 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,13 +148,13 @@ Both do the same thing.

## Authentication

The program will load a `*.key` file according to the configuration file key for the website.
For example, YouTube expects a file `~/youtube.key` with the hexadecimal stream key and so on.
The program loads a JSON file with the stream URL and hexadecimal stream key for the website(s) used.
The user must specify this JSON file location.

### YouTube Live

1. [configure](https://www.youtube.com/live_dashboard) YouTube Live.
2. Edit file `youtube.key` to have the YouTube hexadecimal stream key
2. Edit file `youtube.json` to have the YouTube hexadecimal stream key
3. Run Python script and chosen input will stream on YouTube Live.

```sh
Expand All @@ -168,7 +168,7 @@ Facebook Live requires FFmpeg >= 4.2 due to mandatory RTMPS
1. configure your Facebook Live stream
2. Put stream ID from
[<https://www.facebook.com/live/create>](https://www.facebook.com/live/create)
into the file `~/facebook.key`
into the file `~/facebook.json`
3. Run Python script for Facebook with chosen input

```sh
Expand All @@ -182,7 +182,7 @@ TODO
### Twitch

1. create stream from [Twitch Dashboard](https://dashboard.twitch.tv/settings/channel#stream-preferences). Edit [pylivestream.ini](./src/pylivestream/pylivestream.ini) to have the [closest ingest server](https://stream.twitch.tv/ingests/).
2. put Twitch stream key into file `~/twitch.key`
2. put Twitch stream key into file `~/twitch.json`
3. Run Python script for Twitch with chosen input

```sh
Expand All @@ -196,7 +196,20 @@ Due to the complexity of streaming and the non-specific error codes FFmpeg emits
* [pylivestream.ini](./src/pylivestream/pylivestream.ini) is setup for your computer and desired parameters
* `site` is `facebook`, `twitch`, `youtube`, etc.
* For `pylivestream.camera` and `pylivestream.screen`, more than one `site` can be specified for simultaneous multi-streaming
* remember to setup a `*.key` file with the hexadecimal stream key for EACH site first, OR input the stream key into the "key:" field of your `*.ini` file.
* Setup a JSON file "pylivestream.json" with values you determine, not these dummy values:

```json
{
"facebook": {
"url": "rtmps://their.server",
"streamid": "your-facebook-key",
},
"youtube": {
"url": "rtmp://your.value",
"streamid": "your-key"
}
}
```

[File-Streaming](./File-Streaming.md)

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires = ["setuptools>=61.0.0", "wheel"]

[project]
name = "pylivestream"
version = "1.11.4"
version = "2.0.0"
description = "Livestream using FFmpeg to YouTube Live, Twitter, Facebook Live, Twitch, and more"
keywords = ["youtube", "ffmpeg", "twitch", "twitter live", "facebook live", "restream.io"]
classifiers = [
Expand Down
2 changes: 1 addition & 1 deletion src/pylivestream/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pylivestream.api as pls
pls.microphone('localhost')
pls.microphone('twitch', key='~/twitch.key')
pls.microphone('twitch')
"""

from __future__ import annotations
Expand Down
24 changes: 9 additions & 15 deletions src/pylivestream/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def __init__(self, inifn: Path, site: str, **kwargs) -> None:

self.site = site.lower()

self.osparam(kwargs.get("key"))
self.osparam()

self.docheck = kwargs.get("docheck")

Expand Down Expand Up @@ -50,11 +50,10 @@ def __init__(self, inifn: Path, site: str, **kwargs) -> None:

cmd.extend(self.timelimit) # terminate output after N seconds, IF specified

streamid: str = self.key if self.key else ""

streamid = self.streamid if hasattr(self, "streamid") else ""
# cannot have double quotes for Mac/Linux,
# but need double quotes for Windows
sink: str = self.server + streamid
sink: str = self.url + "/" + streamid
if os.name == "nt":
sink = '"' + sink + '"'

Expand Down Expand Up @@ -84,15 +83,10 @@ def startlive(self, sinks: list[str] = None):

proc = None
# %% special cases for localhost tests
if self.key is None and self.site != "localhost-test":
if self.site == "localhost":
proc = self.F.listener() # start own RTMP server
else:
print(
"A livestream key was not provided or found. Here is the command I would have run:"
)
print("\n", " ".join(self.cmd), "\n", flush=True)
return
if self.site == "localhost-test":
pass
elif self.site == "localhost":
proc = self.F.listener() # start own RTMP server

if proc is not None and proc.poll() is not None:
# listener stopped prematurely, probably due to error
Expand Down Expand Up @@ -265,7 +259,7 @@ def __init__(self, inifn: Path, outfn: Path = None, **kwargs):

self.outfn = Path(outfn).expanduser() if outfn else None

self.osparam(kwargs.get("key"))
self.osparam()

vidIn: list[str] = self.videoIn()
vidOut: list[str] = self.videoOut()
Expand Down Expand Up @@ -301,7 +295,7 @@ def unify_streams(streams: typing.Mapping[str, Stream]) -> str:
so "tee" output can generate multiple streams.
First try: use stream with lowest video bitrate.
Exploits that Python >= 3.6 has guaranteed dict() ordering.
Exploits that Python has guaranteed dict() ordering.
fast native Python argmin()
https://stackoverflow.com/a/11825864
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/camera.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@
help="site to stream, e.g. localhost youtube facebook twitch",
nargs="+",
)
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-y", "--yes", help="no confirmation dialog", action="store_true")
p.add_argument("-t", "--timeout", help="stop streaming after --timeout seconds", type=int)
P = p.parse_args()

stream_webcam(ini_file=P.ini, websites=P.websites, assume_yes=P.yes, timeout=P.timeout)
stream_webcam(ini_file=P.json, websites=P.websites, assume_yes=P.yes, timeout=P.timeout)
4 changes: 2 additions & 2 deletions src/pylivestream/glob.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def cli():
nargs="+",
)
p.add_argument("-glob", help="file glob pattern to stream.")
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-image", help="static image to display, for audio-only files.")
p.add_argument("-shuffle", help="shuffle the globbed file list", action="store_true")
p.add_argument("-loop", help="repeat the globbed file list endlessly", action="store_true")
Expand All @@ -118,7 +118,7 @@ def cli():
P = p.parse_args()

stream_files(
ini_file=P.ini,
ini_file=P.json,
websites=P.websites,
assume_yes=P.yes,
timeout=P.timeout,
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/loopfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,13 @@
help="site to stream, e.g. localhost youtube facebook twitch",
nargs="+",
)
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-y", "--yes", help="no confirmation dialog", action="store_true")
p.add_argument("-t", "--timeout", help="stop streaming after --timeout seconds", type=int)
P = p.parse_args()

stream_file(
ini_file=P.ini,
ini_file=P.json,
websites=P.websites,
assume_yes=P.yes,
timeout=P.timeout,
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/microphone.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,13 @@
nargs="+",
)
p.add_argument("-image", help="static image to display.")
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-y", "--yes", help="no confirmation dialog", action="store_true")
p.add_argument("-t", "--timeout", help="stop streaming after --timeout seconds", type=int)
P = p.parse_args()

stream_microphone(
ini_file=P.ini,
ini_file=P.json,
websites=P.websites,
assume_yes=P.yes,
timeout=P.timeout,
Expand Down
29 changes: 2 additions & 27 deletions src/pylivestream/pylivestream.ini
Original file line number Diff line number Diff line change
Expand Up @@ -40,42 +40,17 @@ hcam: v4l2

# per-site config
[localhost]
server: rtmp://localhost
url: rtmp://localhost

[youtube]
server: rtmp://a.rtmp.youtube.com/live2/
key: ~/youtube.key

[periscope]
video_kbps: 2500
audio_bps: 128k
server: rtmp://va.pscp.tv:80/x/
key: ~/periscope.key
[twitch]

[facebook]
server: rtmps://live-api-s.facebook.com:443/rtmp/
key: ~/facebook.key

[restream.io]
video_kbps: 2500
audio_bps: 128k
server: rtmp://us-east.restream.io/live/
key: ~/restreamio.key

[twitch]
audio_bps: 96k
server: rtmp://live-jfk.twitch.tv/app/
key: ~/twitch.key

[ustream]
keyframe_sec: 1
audio_bps: 128k
server:
key: ~/ustream.key

[vimeo]
server: rtmp://rtmp.cloud.vimeo.com/
key: ~/vimeo.key

[file]
video_kbps: 2000
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/screen.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ def cli():
help="site to stream, e.g. localhost youtube facebook twitch",
nargs="+",
)
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-y", "--yes", help="no confirmation dialog", action="store_true")
p.add_argument("-t", "--timeout", help="stop streaming after --timeout seconds", type=int)
P = p.parse_args()

stream_screen(ini_file=P.ini, websites=P.websites, assume_yes=P.yes, timeout=P.timeout)
stream_screen(ini_file=P.json, websites=P.websites, assume_yes=P.yes, timeout=P.timeout)


if __name__ == "__main__":
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/screen2disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@

p = ArgumentParser()
p.add_argument("outfn", help="video file to save to disk.")
p.add_argument("-i", "--ini", help="*.ini file with stream parameters")
p.add_argument("json", help="JSON file with stream parameters such as key")
p.add_argument("-y", "--yes", help="no confirmation dialog", action="store_true")
p.add_argument("-t", "--timeout", help="stop streaming after --timeout seconds", type=int)
P = p.parse_args()

capture_screen(ini_file=P.ini, out_file=P.outfn, assume_yes=P.yes, timeout=P.timeout)
capture_screen(ini_file=P.json, out_file=P.outfn, assume_yes=P.yes, timeout=P.timeout)
28 changes: 13 additions & 15 deletions src/pylivestream/stream.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import logging
import os
import sys
import json
from configparser import ConfigParser

from . import utils
Expand Down Expand Up @@ -31,7 +32,7 @@ def __init__(self, inifn: Path, site: str, **kwargs):

self.loglevel: list[str] = self.F.INFO if kwargs.get("verbose") else self.F.ERROR

self.inifn: Path = Path(inifn).expanduser() if inifn else None
self.inifn: Path = Path(inifn).expanduser().resolve(strict=True)

self.site: str = site
self.vidsource = kwargs.get("vidsource")
Expand All @@ -52,24 +53,20 @@ def __init__(self, inifn: Path, site: str, **kwargs):

self.timelimit: list[str] = self.F.timelimit(kwargs.get("timeout"))

def osparam(self, key: str):
def osparam(self) -> None:
"""load OS specific config"""

C = ConfigParser(inline_comment_prefixes=("#", ";"))
if self.inifn is None:
logging.info("using package default pylivestream.ini")
cfg = utils.get_inifile("pylivestream.ini")
else:
cfg = Path(self.inifn).expanduser().read_text()
fn = utils.get_inifile("pylivestream.ini")

C.read_string(cfg)
C.read_string(fn.read_text())

self.exe = get_exe(C.get(sys.platform, "exe", fallback="ffmpeg"))
self.probeexe = get_exe(C.get(sys.platform, "ffprobe_exe", fallback="ffprobe"))

if self.site not in C:
raise ValueError(
f"streaming site {self.site} not found in configuration file {self.inifn}"
f"streaming site {self.site} not found in configuration files {fn} {self.inifn}"
)

if "XDG_SESSION_TYPE" in os.environ:
Expand Down Expand Up @@ -120,12 +117,13 @@ def osparam(self, key: str):

self.keyframe_sec: int = C.getint(self.site, "keyframe_sec")

self.server: str = C.get(self.site, "server", fallback=None)
# %% Key (hexaecimal stream ID)
if key:
self.key: str = utils.getstreamkey(key)
else:
self.key = utils.getstreamkey(C.get(self.site, "key", fallback=None))
self.url: str = C.get(self.site, "url", fallback=None)

# %% user JSON (overrides defaults)
cfg = json.loads(Path(self.inifn).expanduser().read_text())
scfg = cfg[self.site]
for k in scfg:
setattr(self, k, scfg[k])

def videoIn(self, quick: bool = False) -> list[str]:
"""
Expand Down
25 changes: 2 additions & 23 deletions src/pylivestream/tests/test_class.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
from pytest import approx
from pathlib import Path
import importlib.resources

import pylivestream as pls
Expand All @@ -10,29 +11,7 @@ def test_get_ini_file(fn):

cfg = pls.utils.get_inifile(fn)

assert isinstance(cfg, str)


def test_key(tmp_path):
"""tests reading of stream key: string and file"""
assert pls.utils.getstreamkey("abc123") == "abc123"
fn = tmp_path / "peri.key"
fn.write_text("abc432")
assert pls.utils.getstreamkey(fn) == "abc432"


@pytest.mark.parametrize("key", ["", None], ids=["empty string", "None"])
def test_empty_key(key):
assert pls.utils.getstreamkey(key) is None


def test_bad_key(tmp_path):

with pytest.raises(IsADirectoryError):
assert pls.utils.getstreamkey(tmp_path) is None

with pytest.raises(FileNotFoundError):
assert pls.utils.getstreamkey(tmp_path / "notAFile.key") is None
assert isinstance(cfg, Path)


@pytest.mark.parametrize("rex", ("ffmpeg", "ffprobe"))
Expand Down
4 changes: 2 additions & 2 deletions src/pylivestream/tests/test_filein.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
def test_props():

with importlib.resources.path("pylivestream.data", "bunny.avi") as fn:
S = pls.FileIn(inifn=None, websites=sites, infn=fn, key="abc")
S = pls.FileIn(inifn=None, websites=sites, infn=fn)
for s in S.streams:
assert "-re" in S.streams[s].cmd
assert S.streams[s].fps == approx(24.0)
Expand All @@ -31,7 +31,7 @@ def test_audio():
with importlib.resources.path(
"pylivestream.data", "logo.png"
) as logo, importlib.resources.path("pylivestream.data", "orch_short.ogg") as fn:
S = pls.FileIn(inifn=None, websites=sites, infn=fn, image=logo, key="abc")
S = pls.FileIn(inifn=None, websites=sites, infn=fn, image=logo)
for s in S.streams:
assert "-re" in S.streams[s].cmd
assert S.streams[s].fps is None
Expand Down
Loading

0 comments on commit 3d52536

Please sign in to comment.