Skip to content

Commit

Permalink
Improvements
Browse files Browse the repository at this point in the history
Add NoSound class
Use subprocess.call (3.4) or .run (3.5) for FFmpeg calls
  • Loading branch information
D. MacCarthy committed Mar 9, 2017
1 parent 3c2fc04 commit 19f4b27
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 66 deletions.
4 changes: 2 additions & 2 deletions sc8pr/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
__version__ = "1.0.1"
__dev__ = True
__version__ = "1.1.0"
__dev__ = True
99 changes: 99 additions & 0 deletions sc8pr/ffrun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Copyright 2015-2017 D.G. MacCarthy <http://dmaccarthy.github.io>
#
# This file is part of "sc8pr".
#
# "sc8pr" is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# "sc8pr" is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with "sc8pr". If not, see <http://www.gnu.org/licenses/>.


"Run FFmpeg as a subprocess"


from os.path import exists
import subprocess as sp

if hasattr(sp, "run"):
# Python 3.5+: Use subprocess.run
def run(args): return sp.run(args, stdout=sp.PIPE, stderr=sp.PIPE)

else:
# Python 3.4-: Use subprocess.call

from tempfile import TemporaryFile

class CompletedProcess:

def __init__(self, args, code, out, err):
self.returncode = code
self.stdout = out
self.stderr = err
self.args =args

def __str__(self):
s = "{}(args={}, returncode={}, stdout={}, stderr={})"
return s.format(type(self).__name__, self.args,
self.returncode, self.stdout, self.stderr)


def run(args):
with TemporaryFile("w+b") as out:
with TemporaryFile("w+b") as err:
code = sp.call(args, stdout=out, stderr=err)
out.seek(0)
outb = out.read()
err.seek(0)
errb = err.read()
return CompletedProcess(args, code, outb, errb)


class FF:
"Static class for encoding using FFmpeg"

AUDIO = 1
VIDEO = 2
cmd = "ffmpeg"

@staticmethod
def _exists(fn, n=1):
"Raise an exception if the destination file exists"
fn = fn.format(n)
if exists(fn): raise FileExistsError(fn)

@staticmethod
def run(args): return run([FF.cmd] + args)

@staticmethod
def convert(src, dest, av=3):
"Convert media to another format using container defaults"
FF._exists(dest)
codec = [["-vn"], ["-an"], []][av - 1]
return FF.run(["-i", src] + codec + [dest])

@staticmethod
def encode(src, dest, fps=30, **kwargs):
"Encode a sequence of images as a video stream"
fps = str(fps)
n = kwargs.get("start")
FF._exists(dest, n if n is not None else 1)
cmd = ["-f", "image2", "-r", fps]
if n: cmd.extend(["-start_number", str(n)])
cmd.extend(["-i", src, "-r", fps])
for key in ("vcodec", "pix_fmt", "vframes"): cmd.extend(FF._opt(kwargs, key))
cmd.append(dest)
return FF.run(cmd)

@staticmethod
def _opt(options, key):
"Get an FFmpeg option from the dictionary"
val = options.get(key)
return ["-" + key, str(val)] if val else []
13 changes: 5 additions & 8 deletions sc8pr/sketch.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@


from sc8pr.papplet import PApplet
from sc8pr.util import step, logError, CENTER, rectAnchor, addToMap, setCursor, tempDir
from sc8pr.util import step, logError, CENTER, rectAnchor, addToMap, setCursor, tempDir, loadSound
from sc8pr.gui import GUI
from sc8pr.io import prompt, fileDialog, USERINPUT
from sc8pr.grid import OPEN, SAVE, FOLDER
Expand All @@ -27,7 +27,7 @@
from math import hypot, cos, sin, sqrt
import pygame
from pygame import display
from pygame.mixer import Sound
from sys import stderr


# Status constants...
Expand Down Expand Up @@ -781,16 +781,13 @@ def loadSounds(self, *args):
for s in args:
if type(s) is str: key = s
else: s, key = s
try: self._sounds[key] = Sound(s)
except: logError()
self._sounds[key] = _sound(s)

def sound(self, key, cache=True, **kwargs):
"Play a sound"
snd = self._sounds.get(key)
if not cache or snd is None:
try:
snd = Sound(key)
if cache: self._sounds[key] = snd
except: logError()
snd = loadSound(key)
if cache: self._sounds[key] = snd
if snd: snd.play(**kwargs)
return snd
34 changes: 19 additions & 15 deletions sc8pr/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,27 +22,13 @@
from tempfile import mkdtemp
from random import randint
from pygame import Color, Rect
from pygame.mixer import Sound
from pygame.constants import QUIT, KEYDOWN, KEYUP, MOUSEMOTION, MOUSEBUTTONDOWN as MOUSEDOWN, MOUSEBUTTONUP as MOUSEUP, VIDEORESIZE as RESIZE
from subprocess import call
from tempfile import mkstemp
import pygame, sc8pr, os


def run(*cmd, **kwargs):
"Run a command using subprocess.call"
out, outName = mkstemp(dir=".")
err, errName = mkstemp(dir=".")
code = call(cmd, stdout=out, stderr=err, **kwargs)
return dict(code=code, out=_tidy(out, outName), err=_tidy(err, errName))

def _tidy(h, name):
"Read data and dispose of temporary file"
os.close(h)
with open(name) as f: data = f.read()
os.remove(name)
return data


# Type styles...
BOLD = 1
ITALIC = 2
Expand Down Expand Up @@ -248,6 +234,24 @@ def step(dt, *args):
return [_step(dt, *args[i:]) for i in range(len(args)-1)]


# Prevent crash when using sound files that cannot be loaded

class NoSound:
"A class to represent Sound objects that fail to load"
def play(self, **kwargs): pass

__init__ = play
set_volume = play

def loadSound(fn):
"Attempt to load a sound file using pygame.mixer.Sound"
try: s = Sound(fn)
except:
s = NoSound()
print("Unable to load {}".format(fn), file=stderr)
return s


# Classes...

class StaticClassException(Exception):
Expand Down
46 changes: 5 additions & 41 deletions sc8pr/video.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2015-2016 D.G. MacCarthy <http://dmaccarthy.github.io>
# Copyright 2015-2017 D.G. MacCarthy <http://dmaccarthy.github.io>
#
# This file is part of "sc8pr".
#
Expand All @@ -18,14 +18,14 @@

from sc8pr.image import Image
from sc8pr.effects import ScriptSprite
from sc8pr.util import jdump, jload, defaultExtension, tempDir, run
from sc8pr.util import jdump, jload, defaultExtension, tempDir
import sc8pr

from sys import version_info as ver, stderr
from zipfile import ZipFile
from struct import pack, unpack
from os.path import isfile
import pygame, zlib, os
import pygame, zlib


class ZImage:
Expand Down Expand Up @@ -82,7 +82,7 @@ class Video:
_pending = 0

info = dict(python=(ver.major, ver.minor, ver.micro),
sc8pr=sc8pr.version, format=ZImage.format)
sc8pr=sc8pr.__version__, format=ZImage.format)

def __init__(self, *archive, interval=None, gui=False, wait=True):
self.data = []
Expand Down Expand Up @@ -193,11 +193,10 @@ def clip(self, start=0, end=None):
v.data = self.data[start:end]
return v

def export(self, path="?/img{}.png", pattern="05d", start=0, file=None, ffmpeg=None, **kwargs):
def export(self, path="?/img{}.png", pattern="05d", start=0, file=None, **kwargs):
"Export the images as individual files"
if self.pending: self.buffer()
path = tempDir(path)
ffpath = path.format("%{}".format(pattern))
path = path.format("{{:{}}}".format(pattern))
i = 0
if file is None: file = self.output
Expand All @@ -207,44 +206,9 @@ def export(self, path="?/img{}.png", pattern="05d", start=0, file=None, ffmpeg=N
img.image.saveAs(path.format(i + start))
i += 1
if file and i % 50 == 0: print(i, file=file)
if ffmpeg:
if file: print("Calling FFMPEG...", file=file)
if start:
option = {"start":start}
option.update(kwargs)
else: option = kwargs
data = FF.encode(ffpath, ffmpeg=ffmpeg, **option)
if file: print(data["err"] if data["code"] else "Saved video!", file=file)
return path


class FF:
"Static class for encoding using FFMPEG"

cmd = "ffmpeg"

@staticmethod
def encode(src, out="video.mp4", ffmpeg=True, fps=30, **kwargs):
"Convert a sequence of images into a video using FFMPEG"
if isfile(out):
if kwargs.get("overwrite") is True: os.remove(out)
else: return dict(code=1, err="File already exists: {}".format(out))
fps = str(fps)
n = kwargs.get("start")
args = ["-start_number", str(n)] if n else []
args = ["-f", "image2", "-r", fps] + args + ["-i", src, "-r", fps]
for key in ("vcodec", "pix_fmt"): args.extend(FF.opt(kwargs, key))
args.append(out)
if ffmpeg is True: ffmpeg = FF.cmd
return run(ffmpeg, *args)

@staticmethod
def opt(options, key):
"Get an FFMPEG option from the dictionary"
val = options.get(key)
return ["-" + key, val] if val else []


class VideoSprite(ScriptSprite):
"A sprite whose costumes are extracted as needed from a Video instance"

Expand Down

0 comments on commit 19f4b27

Please sign in to comment.