python-codecs: wip

This commit is contained in:
Koushik Dutta
2023-03-11 00:16:49 -08:00
parent 601cd39ba4
commit 4570f9cd38
11 changed files with 365 additions and 0 deletions

5
plugins/python-codecs/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
.DS_Store
out/
node_modules/
dist/
.venv

View File

@@ -0,0 +1,9 @@
.DS_Store
out/
node_modules/
*.map
fs
src
.vscode
dist/*.js
.venv

View File

@@ -0,0 +1,41 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Scrypted Debugger",
"type": "python",
"request": "attach",
"connect": {
"host": "${config:scrypted.debugHost}",
"port": 10081
},
"justMyCode": false,
"preLaunchTask": "scrypted: deploy+debug",
"pathMappings": [
{
"localRoot": "/Volumes/Dev/scrypted/server/python/",
"remoteRoot": "/Volumes/Dev/scrypted/server/python/",
},
{
"localRoot": "${workspaceFolder}/src",
"remoteRoot": "${config:scrypted.pythonRemoteRoot}"
},
]
},
{
"name": "Python: Test",
"type": "python",
"request": "launch",
"program": "${workspaceFolder}/src/test.py",
"console": "internalConsole",
"justMyCode": true,
"env": {
"GST_PLUGIN_PATH": "/opt/homebrew/lib/gstreamer-1.0"
}
}
]
}

View File

@@ -0,0 +1,19 @@
{
// docker installation
// "scrypted.debugHost": "koushik-thin",
// "scrypted.serverRoot": "/server",
// pi local installation
// "scrypted.debugHost": "192.168.2.119",
// "scrypted.serverRoot": "/home/pi/.scrypted",
// local checkout
"scrypted.debugHost": "127.0.0.1",
"scrypted.serverRoot": "/Users/koush/.scrypted",
"scrypted.pythonRemoteRoot": "${config:scrypted.serverRoot}/volume/plugin.zip",
"python.analysis.extraPaths": [
"./node_modules/@scrypted/sdk/types/scrypted_python"
]
}

View File

@@ -0,0 +1,20 @@
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "scrypted: deploy+debug",
"type": "shell",
"presentation": {
"echo": true,
"reveal": "silent",
"focus": false,
"panel": "shared",
"showReuseMessage": true,
"clear": false
},
"command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}",
},
]
}

View File

@@ -0,0 +1,3 @@
# Python Codecs for Scrypted
Adds encoding and decoding capabilites to Scrypted via Gstreamer and Libav.

57
plugins/python-codecs/package-lock.json generated Normal file
View File

@@ -0,0 +1,57 @@
{
"name": "@scrypted/python-codecs",
"version": "0.0.23",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@scrypted/python-codecs",
"version": "0.0.23",
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
}
},
"../../sdk": {
"name": "@scrypted/sdk",
"version": "0.2.85",
"dev": true,
"license": "ISC",
"dependencies": {
"@babel/preset-typescript": "^7.18.6",
"adm-zip": "^0.4.13",
"axios": "^0.21.4",
"babel-loader": "^9.1.0",
"babel-plugin-const-enum": "^1.1.0",
"esbuild": "^0.15.9",
"ncp": "^2.0.0",
"raw-loader": "^4.0.2",
"rimraf": "^3.0.2",
"tmp": "^0.2.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"webpack": "^5.75.0",
"webpack-bundle-analyzer": "^4.5.0"
},
"bin": {
"scrypted-changelog": "bin/scrypted-changelog.js",
"scrypted-debug": "bin/scrypted-debug.js",
"scrypted-deploy": "bin/scrypted-deploy.js",
"scrypted-deploy-debug": "bin/scrypted-deploy-debug.js",
"scrypted-package-json": "bin/scrypted-package-json.js",
"scrypted-setup-project": "bin/scrypted-setup-project.js",
"scrypted-webpack": "bin/scrypted-webpack.js"
},
"devDependencies": {
"@types/node": "^18.11.18",
"@types/stringify-object": "^4.0.0",
"stringify-object": "^3.3.0",
"ts-node": "^10.4.0",
"typedoc": "^0.23.21"
}
},
"node_modules/@scrypted/sdk": {
"resolved": "../../sdk",
"link": true
}
}
}

View File

@@ -0,0 +1,34 @@
{
"name": "@scrypted/python-codecs",
"description": "Scrypted Python Codecs",
"keywords": [
"scrypted",
"plugin",
"codecs"
],
"scripts": {
"scrypted-setup-project": "scrypted-setup-project",
"prescrypted-setup-project": "scrypted-package-json",
"build": "scrypted-webpack",
"prepublishOnly": "NODE_ENV=production scrypted-webpack",
"prescrypted-vscode-launch": "scrypted-webpack",
"scrypted-vscode-launch": "scrypted-deploy-debug",
"scrypted-deploy-debug": "scrypted-deploy-debug",
"scrypted-debug": "scrypted-debug",
"scrypted-deploy": "scrypted-deploy",
"scrypted-readme": "scrypted-readme",
"scrypted-package-json": "scrypted-package-json"
},
"scrypted": {
"name": "Python Codecs",
"runtime": "python",
"type": "API",
"interfaces": [
"VideoFrameGenerator"
]
},
"devDependencies": {
"@scrypted/sdk": "file:../../sdk"
},
"version": "0.0.23"
}

View File

@@ -0,0 +1,124 @@
import concurrent.futures
import threading
import asyncio
from queue import Queue
try:
import gi
gi.require_version('Gst', '1.0')
gi.require_version('GstBase', '1.0')
from gi.repository import GLib, GObject, Gst
GObject.threads_init()
Gst.init(None)
except:
pass
class Callback:
def __init__(self, callback) -> None:
self.loop = asyncio.get_event_loop()
self.callback = callback
def createPipelineIterator(pipeline: str):
pipeline = '{pipeline} ! appsink name=appsink emit-signals=true sync=false'.format(pipeline=pipeline)
gst = Gst.parse_launch(pipeline)
bus = gst.get_bus()
def on_bus_message(bus, message):
t = str(message.type)
print(t)
if t == str(Gst.MessageType.EOS):
finish()
elif t == str(Gst.MessageType.WARNING):
err, debug = message.parse_warning()
print('Warning: %s: %s\n' % (err, debug))
elif t == str(Gst.MessageType.ERROR):
err, debug = message.parse_error()
print('Error: %s: %s\n' % (err, debug))
finish()
def stopGst():
bus.remove_signal_watch()
bus.disconnect(watchId)
gst.set_state(Gst.State.NULL)
def finish():
nonlocal hasFinished
hasFinished = True
callback = Callback(None)
callbackQueue.put(callback)
if not asyncFuture.done():
asyncFuture.set_result(None)
if not finished.done():
finished.set_result(None)
watchId = bus.connect('message', on_bus_message)
bus.add_signal_watch()
finished = concurrent.futures.Future()
finished.add_done_callback(lambda _: stopGst())
hasFinished = False
appsink = gst.get_by_name('appsink')
callbackQueue = Queue()
asyncFuture = asyncio.Future()
async def gen():
try:
while True:
nonlocal asyncFuture
asyncFuture = asyncio.Future()
yieldFuture = asyncio.Future()
async def asyncCallback(sample):
asyncFuture.set_result(sample)
await yieldFuture
callbackQueue.put(Callback(asyncCallback))
sample = await asyncFuture
if not sample:
break
yield sample
yieldFuture.set_result(None)
finally:
finish()
def on_new_sample(sink, preroll):
nonlocal hasFinished
sample = sink.emit('pull-preroll' if preroll else 'pull-sample')
callback: Callback = callbackQueue.get()
if not callback.callback or hasFinished:
hasFinished = True
if callback.callback:
asyncio.run_coroutine_threadsafe(callback.callback(None), loop = callback.loop)
return Gst.FlowReturn.OK
future = asyncio.run_coroutine_threadsafe(callback.callback(sample), loop = callback.loop)
future.result()
return Gst.FlowReturn.OK
appsink.connect('new-preroll', on_new_sample, True)
appsink.connect('new-sample', on_new_sample, False)
gst.set_state(Gst.State.PLAYING)
return gst, gen
def mainThread():
async def asyncMain():
gst, gen = createPipelineIterator('rtspsrc location=rtsp://localhost:59668/18cc179a814fd5b3 ! rtph264depay ! h264parse ! vtdec_hw ! videoconvert ! video/x-raw')
i = 0
async for sample in gen():
print('sample')
i = i + 1
if i == 10:
break
loop = asyncio.new_event_loop()
asyncio.ensure_future(asyncMain(), loop = loop)
loop.run_forever()
if __name__ == "__main__":
threading.Thread(target = mainThread).start()
mainLoop = GLib.MainLoop()
mainLoop.run()

View File

@@ -0,0 +1,48 @@
import scrypted_sdk
from typing import Any
from urllib.parse import urlparse
def optional_chain(root, *keys):
result = root
for k in keys:
if isinstance(result, dict):
result = result.get(k, None)
else:
result = getattr(result, k, None)
if result is None:
break
return result
class PythonCodecs(scrypted_sdk.ScryptedDeviceBase, scrypted_sdk.VideoFrameGenerator):
def __init__(self, nativeId = None):
super().__init__(nativeId)
async def generateVideoFrames(self, mediaObject: scrypted_sdk.MediaObject, options: scrypted_sdk.VideoFrameGeneratorOptions = None, filter: Any = None) -> scrypted_sdk.VideoFrame:
ffmpegInput: scrypted_sdk.FFmpegInput = await scrypted_sdk.mediaManager.convertMediaObjectToJSON(mediaObject, scrypted_sdk.ScryptedMimeTypes.FFmpegInput.value)
container = ffmpegInput.get('container', None)
videosrc = ffmpegInput.get('url')
videoCodec = optional_chain(ffmpegInput, 'mediaStreamOptions', 'video', 'codec')
if videosrc.startswith('tcp://'):
parsed_url = urlparse(videosrc)
videosrc = 'tcpclientsrc port=%s host=%s' % (
parsed_url.port, parsed_url.hostname)
if container == 'mpegts':
videosrc += ' ! tsdemux'
elif container == 'sdp':
videosrc += ' ! sdpdemux'
else:
raise Exception('unknown container %s' % container)
elif videosrc.startswith('rtsp'):
videosrc = 'rtspsrc buffer-mode=0 location=%s protocols=tcp latency=0 is-live=false' % videosrc
if videoCodec == 'h264':
videosrc += ' ! rtph264depay ! h264parse'
try:
while True:
yield 1
finally:
print('done!')
def create_scrypted_plugin():
return PythonCodecs()

View File

@@ -0,0 +1,5 @@
# plugin
PyGObject>=3.30.4; sys_platform != 'win32'
# libav doesnt work on arm7
av>=10.0.0; sys_platform != 'linux' or platform_machine == 'x86_64' or platform_machine == 'aarch64'
pyvips