Skip to content

Commit

Permalink
Squashed commit of the following:
Browse files Browse the repository at this point in the history
commit 01c5ce2
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Fri Jan 11 14:48:35 2019 -0500

    checkout display too

commit cd013d3
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Fri Jan 11 14:35:55 2019 -0500

    fixture scope

commit 468bca7
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Fri Jan 11 13:42:28 2019 -0500

    check device before full stream attempt

commit e49c12a
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Fri Jan 11 02:24:39 2019 -0500

    catch listener errors and skip

commit d97e21f
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Fri Jan 11 00:38:30 2019 -0500

    added missing listener for file test

commit 28cae17
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Thu Jan 10 23:32:37 2019 -0500

    screenshare verify working

    microphone verify working

    verify webcam ok

    filein check

commit 96a61de
Author: Michael Hirsch, Ph.D <scivision@users.noreply.github.com>
Date:   Thu Jan 10 22:52:49 2019 -0500

    quick device check screen
  • Loading branch information
scivision committed Jan 11, 2019
1 parent ae26965 commit a9cd412
Show file tree
Hide file tree
Showing 9 changed files with 183 additions and 73 deletions.
22 changes: 13 additions & 9 deletions .travis.yml
@@ -1,3 +1,6 @@
# travis-only windows bug (appveyor windows fine)
# temporary macos travis bug (worked before)

language: python
group: travis_latest
dist: xenial
Expand Down Expand Up @@ -25,15 +28,16 @@ matrix:
after_success:
- pytest --cov --cov-config=setup.cfg
- coveralls
- os: osx
language: sh
before_install: brew install ffmpeg
install: pip3 install -e .[tests]
- os: windows
language: sh
before_install: # test with FFmpeg on AppVeyor
- choco install python3
- export PATH="/c/Python37:/c/Python37/Scripts:$PATH"
# - os: osx
# language: sh
# before_install: brew install ffmpeg > /dev/null
# install: pip3 install -e .[tests]
# - os: windows
# language: sh
# before_install: # test with FFmpeg on AppVeyor
# - choco install ffmpeg
# - choco install python3
# - export PATH="/c/Python37:/c/Python37/Scripts:$PATH"

install: pip install -e .[tests]

Expand Down
27 changes: 21 additions & 6 deletions pylivestream/__init__.py
Expand Up @@ -38,12 +38,12 @@ def __init__(self,
cmd: List[str] = []
cmd.append(self.exe)

cmd.extend(self.loglevel)
cmd.extend(self.yes)
cmd += self.loglevel
cmd += self.yes

# cmd.extend(self.timelimit) # terminate input after N seconds, IF specified
# cmd += self.timelimit # terminate input after N seconds, IF specified

cmd.extend(self.queue)
cmd += self.queue

cmd += vidIn + audIn

Expand All @@ -60,9 +60,20 @@ def __init__(self,
self.sink: List[str] = [self.server + streamid]

self.cmd: List[str] = cmd + self.sink
# %% quick check command, to verify device exists
self.checkcmd = ([self.exe] + self.loglevel + ['-t', '0.1'] +
self.videoIn(quick=True) +
self.audioIn(quick=True) +
['-t', '0.1'] + # webcam needs at output
['-f', 'null', '-']
)

def startlive(self, sinks: Sequence[str] = None):
"""finally start the stream(s)"""

if not self.check_device():
return

proc = None
# %% special cases for localhost tests
if self.key is None and self.site != 'localhost-test':
Expand All @@ -71,6 +82,10 @@ def startlive(self, sinks: Sequence[str] = None):
else:
print('\n', ' '.join(self.cmd), '\n', flush=True)
return

if proc is not None and proc.poll() is not None:
# listener stopped prematurely, probably due to error
raise RuntimeError(f'listener stopped with code {proc.poll()}')
# %% RUN STREAM
cmd: List[str]
if not sinks: # single stream
Expand Down Expand Up @@ -104,8 +119,8 @@ def startlive(self, sinks: Sequence[str] = None):

utils.run(cmd)

# %% this kill the listener before starting the next process, or upon final process closing.
if proc is not None:
# %% stop the listener before starting the next process, or upon final process closing.
if proc is not None and proc.poll() is None:
proc.terminate()
yield

Expand Down
73 changes: 53 additions & 20 deletions pylivestream/stream.py
Expand Up @@ -127,16 +127,16 @@ def osparam(self):
self.key: str = utils.getstreamkey(
C.get(self.site, 'key', fallback=None))

def videoIn(self) -> List[str]:
def videoIn(self, quick: bool = False) -> List[str]:
"""config video input"""
v: List[str]
# %% configure video input
if self.vidsource == 'screen':
v = self.screengrab()
v = self.screengrab(quick)
elif self.vidsource == 'camera':
v = self.webcam()
v = self.webcam(quick)
elif self.vidsource is None or self.vidsource == 'file':
v = self.filein()
v = self.filein(quick)
else:
raise ValueError(f'unknown vidsource {self.vidsource}')

Expand Down Expand Up @@ -166,7 +166,7 @@ def videoOut(self) -> List[str]:

return v

def audioIn(self) -> List[str]:
def audioIn(self, quick: bool = False) -> List[str]:
"""
-ac * may not be needed, took out.
-ac 2 NOT -ac 1 to avoid "non monotonous DTS in output stream" errors
Expand All @@ -175,11 +175,13 @@ def audioIn(self) -> List[str]:
return []

if not self.vidsource == 'file':
return ['-f', self.acap, '-i', self.audiochan]
a = ['-f', self.acap, '-i', self.audiochan]
else: # file input
return []
a = []
# else: # file input
# return ['-ac','2']
# a = ['-ac','2']

return a

def audioOut(self) -> List[str]:
"""select audio codec
Expand Down Expand Up @@ -217,33 +219,43 @@ def video_bitrate(self):
else:
self.video_kbps: int = list(BR60.values())[bisect.bisect_left(list(BR60.keys()), x)]

def screengrab(self) -> List[str]:
"""choose to grab video from desktop. May not work for Wayland."""
v: List[str] = ['-f', self.vcap,
'-r', str(self.fps)]
def screengrab(self, quick: bool = False) -> List[str]:
"""choose to grab video from desktop. May not work for Wayland.
NOTE: for Linux and MacOS, assumes DISPLAY :0.0
"""
v: List[str] = ['-f', self.vcap]

if self.res is not None:
if not quick:
v += ['-r', str(self.fps)]

if not quick and self.res is not None:
v += ['-s', 'x'.join(map(str, self.res))]

if sys.platform == 'linux':
v += ['-i', f':0.0+{self.origin[0]},{self.origin[1]}']
if quick:
v += ['-i', ':0.0']
else:
v += ['-i', f':0.0+{self.origin[0]},{self.origin[1]}']
elif sys.platform == 'win32':
v += ['-offset_x', self.origin[0], '-offset_y', self.origin[1],
'-i', self.videochan]
if quick:
v += ['-i', self.videochan]
else:
v += ['-offset_x', self.origin[0], '-offset_y', self.origin[1],
'-i', self.videochan]
elif sys.platform == 'darwin':
v += ['-i', "0:0"]

return v

def webcam(self) -> List[str]:
def webcam(self, quick: bool = False) -> List[str]:
"""configure webcam"""
v: List[str] = ['-f', self.hcam,
'-i', self.videochan]
# '-r', str(self.fps), # -r causes bad dropouts

return v

def filein(self) -> List[str]:
def filein(self, quick: bool = False) -> List[str]:
"""
used for:
Expand All @@ -268,11 +280,14 @@ def filein(self) -> List[str]:
v.append(self.F.THROTTLE)
# %% image / audio / vidoe cases
if self.staticimage:
v.extend(['-loop', '1', '-f', 'image2', '-i', str(self.image)])
if not quick:
v += ['-loop', '1']
v.extend(['-f', 'image2', '-i', str(self.image)])
elif self.movingimage:
v.extend(self.F.movingBG(self.image))
elif self.loop and not self.image: # loop for traditional video
v.extend(['-stream_loop', '-1']) # FFmpeg >= 3
if not quick:
v.extend(['-stream_loop', '-1']) # FFmpeg >= 3
# %% audio (for image+audio) or video
if self.infn:
v.extend(['-i', str(self.infn)])
Expand All @@ -298,3 +313,21 @@ def buffer(self, server: str) -> List[str]:
buf += ['-f', 'flv']

return buf

def check_device(self, site: str = None) -> bool:
"""
requires stream to have been configured first.
does a quick test stream to "null" to verify device is actually accessible
"""
if not site:
try:
site = self.site
except AttributeError:
site = list(self.streams.keys())[0] # type: ignore

try:
checkcmd = self.checkcmd # type: ignore
except AttributeError:
checkcmd = self.streams[site].checkcmd # type: ignore

return utils.check_device(checkcmd)
27 changes: 27 additions & 0 deletions pylivestream/utils.py
Expand Up @@ -6,6 +6,7 @@
from typing import Tuple, Union, List

DEVNULL = subprocess.DEVNULL
R = Path(__file__).resolve().parents[1] / 'tests/'


def run(cmd: List[str]):
Expand All @@ -29,6 +30,32 @@ def run(cmd: List[str]):
"""


def check_device(cmd: List[str]) -> bool:
try:
run(cmd)
ok = True
except subprocess.CalledProcessError:
logging.critical(f'device not available, test command failed: \n {" ".join(cmd)}')
ok = False

return ok


def check_display(fn: Path = None) -> bool:
"""see if it's possible to display something with a test file"""
if isinstance(fn, Path) and not fn.is_file():
raise FileNotFoundError(str(fn))

if fn is None:
fn = R / 'bunny.avi'

cmd = ['ffplay', '-v', 'error', '-t', '1.0', '-autoexit', str(fn)]

ret = subprocess.run(cmd, timeout=10).returncode

return ret == 0


def getexe(exein: Path = None) -> Tuple[str, str]:
"""checks that host streaming program is installed"""

Expand Down
2 changes: 1 addition & 1 deletion pylivestream/listener.py → tests/conftest.py
Expand Up @@ -4,7 +4,7 @@
import subprocess


@pytest.fixture()
@pytest.fixture(scope='function')
def listener():
"""needed for pytest, regular function doesn't terminate properly under test"""
print('starting RTMP listener')
Expand Down
34 changes: 24 additions & 10 deletions tests/test_filein.py
Expand Up @@ -3,12 +3,10 @@
import pylivestream as pls
import pytest
from pytest import approx
import os
import subprocess
from pylivestream.listener import listener # noqa: F401

CI = bool(os.environ['CI']) if 'CI' in os.environ else False
R = Path(__file__).parent
skip = False

sites = ['periscope', 'youtube', 'facebook']
inifn = R / 'test.ini'
Expand All @@ -19,6 +17,7 @@
S = pls.stream.Stream(inifn, 'localhost-test')
S.osparam()
timelimit = int(S.timelimit[1]) + 3 # allowing 3 seconds leeway
del S


def test_filein_video():
Expand Down Expand Up @@ -50,19 +49,34 @@ def test_filein_audio():
assert S.streams[s].video_kbps == 400


@pytest.mark.skipif(CI, reason="Many CI's don't have audio hardware")
def test_file_simple():
"""stream to localhost"""
s = pls.FileIn(inifn, 'localhost',
"""stream to localhost
no listener fixture, to test the other listener
"""
global skip
# not localhost-test, to test the other listener
S = pls.FileIn(inifn, 'localhost',
R / 'orch_short.ogg',
image=LOGO,
yes=True)
s.golive()

ok = pls.utils.check_display()

if not ok:
skip = True
pytest.skip(f'device display not available')

try:
S.golive()
except RuntimeError as e:
skip = True
pytest.skip(f'listener error {e}')


def test_filein_script(listener):
if skip:
pytest.skip('device not available')

@pytest.mark.usefixtures("listener")
@pytest.mark.skipif(CI, reason="Many CI's don't have audio hardware")
def test_filein_script():
subprocess.check_call(['FileGlobLivestream',
str(VIDFN),
'localhost-test',
Expand Down
23 changes: 14 additions & 9 deletions tests/test_microphone.py
Expand Up @@ -2,12 +2,10 @@
from pathlib import Path
import pylivestream as pls
import pytest
import os
import subprocess
from pylivestream.listener import listener # noqa: F401

CI = bool(os.environ['CI']) if 'CI' in os.environ else False
R = Path(__file__).parent
skip = False

sites = ['periscope', 'youtube', 'facebook']
inifn = R / 'test.ini'
Expand All @@ -18,6 +16,7 @@
S = pls.stream.Stream(inifn, 'localhost-test')
S.osparam()
timelimit = int(S.timelimit[1]) + 3 # allowing 3 seconds leeway
del S


def test_microphone():
Expand Down Expand Up @@ -50,17 +49,23 @@ def test_microphone_4Kbg():
assert S.streams[s].video_kbps == 4000


@pytest.mark.usefixtures("listener")
@pytest.mark.skipif(CI, reason="Many CI's don't have audio hardware")
def test_microphone_stream():
def test_microphone_stream(listener):
global skip
S = pls.Microphone(inifn, 'localhost-test', image=LOGO)

ok = S.check_device()

if not ok:
skip = True
pytest.skip(f'device not available: {S.streams.popitem()[1].checkcmd}')

S.golive()


@pytest.mark.usefixtures("listener")
@pytest.mark.skipif(CI, reason="Many CI's don't have audio hardware")
def test_microphone_script():
def test_microphone_script(listener):
if skip:
pytest.skip(f'device not available')

subprocess.check_call(['MicrophoneLivestream',
'-i', str(inifn),
'localhost-test', '--yes'],
Expand Down

0 comments on commit a9cd412

Please sign in to comment.