From 8903eae46054d8cc3dde989f7cd9cf7db864dfab Mon Sep 17 00:00:00 2001 From: Jonas Weinz Date: Sun, 18 Oct 2020 20:19:26 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + Pipfile | 15 ++ src/gtk_mediathek_player/gst_pipeline.py | 21 ++ src/gtk_mediathek_player/gst_widget.py | 97 ++++++++ src/gtk_mediathek_player/main_window.py | 110 +++++++++ src/gtk_mediathek_player/mediathek_request.py | 185 ++++++++++++++ src/gtk_mediathek_player/player_widget.py | 112 +++++++++ src/gtk_mediathek_player/search_widget.py | 129 ++++++++++ src/gtk_mediathek_player/tools.py | 19 ++ testing/testing.ipynb | 232 ++++++++++++++++++ 10 files changed, 921 insertions(+) create mode 100644 Pipfile create mode 100644 src/gtk_mediathek_player/gst_pipeline.py create mode 100644 src/gtk_mediathek_player/gst_widget.py create mode 100644 src/gtk_mediathek_player/main_window.py create mode 100644 src/gtk_mediathek_player/mediathek_request.py create mode 100644 src/gtk_mediathek_player/player_widget.py create mode 100644 src/gtk_mediathek_player/search_widget.py create mode 100644 src/gtk_mediathek_player/tools.py create mode 100644 testing/testing.ipynb diff --git a/.gitignore b/.gitignore index 7f7cccc..627a1bd 100644 --- a/.gitignore +++ b/.gitignore @@ -58,3 +58,4 @@ docs/_build/ # PyBuilder target/ +.vscode/ diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..bc88337 --- /dev/null +++ b/Pipfile @@ -0,0 +1,15 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true +name = "pypi" + +[packages] +pygobject = "*" +jupyterlab = "*" +ipykernel = "*" + +[dev-packages] +autopep8 = "*" + +[requires] +python_version = "3.8" diff --git a/src/gtk_mediathek_player/gst_pipeline.py b/src/gtk_mediathek_player/gst_pipeline.py new file mode 100644 index 0000000..feeadd5 --- /dev/null +++ b/src/gtk_mediathek_player/gst_pipeline.py @@ -0,0 +1,21 @@ +import gi +gi.require_version('Gst', '1.0') + +from gi.repository import Gst + +def init_gst() -> None: + if not init_gst.gst_initialized: + Gst.init(None) + Gst.init_check(None) + init_gst.gst_initialized = True + +init_gst.gst_initialized = False + + +def create_pipeline(): + init_gst() + + pipeline = Gst.Pipeline() + + return pipeline + diff --git a/src/gtk_mediathek_player/gst_widget.py b/src/gtk_mediathek_player/gst_widget.py new file mode 100644 index 0000000..a3a53e2 --- /dev/null +++ b/src/gtk_mediathek_player/gst_widget.py @@ -0,0 +1,97 @@ +from gi.repository import Gtk, Gst + +from . import gst_pipeline + + +class GstWidget(Gtk.Box): + def __init__(self): + Gtk.Box.__init__(self) + + self._state = Gst.State.NULL + + self.connect('realize', self._on_realize) + + def _on_realize(self, widget): + self._build_sink_and_widget() + widget.pack_start(self._sinkwidget, True, True, 0) + self._create_player() + self._bus = self._player.get_bus() + self._bus.add_signal_watch() + self._bus.connect("message::state-changed", self.on_state_changed) + #self.load_from_uri("https://pdvideosdaserste-a.akamaihd.net/int/2020/05/20/412d8633-dfaf-438c-93d2-f81914d44945/960-1_679663.mp4") + self._sinkwidget.show() + #self.play() + #self._pipeline.set_state(Gst.State.PLAYING) + + def _build_pipeline(self): + self._pipeline = Gst.Pipeline() + + bin = Gst.parse_bin_from_description('videotestsrc', True) + + factory = self._pipeline.get_factory() + gtksink = factory.make('gtksink') + + self._pipeline.add(bin) + + self._pipeline.add(gtksink) + + bin.link(gtksink) + + return gtksink + + def _build_sink_and_widget(self) -> None: + gtkglsink = Gst.ElementFactory.make("gtkglsink") + + # TODO: fallback if not graphic acceleration is available + + sinkbin = Gst.ElementFactory.make("glsinkbin") + sinkbin.set_property("sink", gtkglsink) + + self._sink = sinkbin + + self._sinkwidget = gtkglsink.get_property("widget") + + def _create_player(self) -> None: + self._player = Gst.ElementFactory.make("playbin", "player") + self._player.set_property("video-sink", self._sink) + self._state = Gst.State.NULL + + + def load_from_uri(self, url:str) -> None: + self.stop() + self._player.set_property("uri", url) + + def play(self): + self._player.set_state(Gst.State.PLAYING) + + def pause(self): + self._player.set_state(Gst.State.PAUSED) + + def stop(self): + self._player.set_state(Gst.State.NULL) + + def get_state(self): + return self._state + + def get_duration(self): + return self._player.query_duration(Gst.Format.TIME)[1] / Gst.SECOND + + def get_position(self): + return self._player.query_position(Gst.Format.TIME)[1] / Gst.SECOND + + def set_position(self, position): + if self._state == Gst.State.PLAYING or self._state == Gst.State.PAUSED: + self._player.seek_simple( + Gst.Format.TIME, + Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT, + position * Gst.SECOND + ) + + + def on_state_changed(self, bus, msg): + old, new, pending = msg.parse_state_changed() + if not msg.src == self._player: + # not from the playbin, ignore + return + + self._state = new diff --git a/src/gtk_mediathek_player/main_window.py b/src/gtk_mediathek_player/main_window.py new file mode 100644 index 0000000..13f0990 --- /dev/null +++ b/src/gtk_mediathek_player/main_window.py @@ -0,0 +1,110 @@ +import gi + +from gtk_mediathek_player.tools import new_button_with_icon, new_radio_button_with_icon + +gi.require_version("Gtk", "3.0") +from gi.repository import Gtk, Gio + +from . import player_widget +from . import search_widget +from . import tools +from . import mediathek_request as mr + +class MainApp(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self, title="Gtk Mediathek Player") + + self.set_default_size(800, 600) + + self._main_container = Gtk.Overlay() + + self._main_stack = Gtk.Stack() + + self._fullscreen = False + + self._player_widget = self._create_player_widget() + self._search_widget = self._create_search_widget() + + self._main_stack.add_named(self._player_widget, "player") + self._main_stack.add_named(self._search_widget, "search") + + self._main_stack.show_all() + + self.set_active_pane("search") + + + self._main_container.add(self._main_stack) + + self._headerbar = self._create_headerbar(True) + + + #self._main_container.add_overlay(self._create_fullscreen_bar()) + + self.add(self._main_container) + + + def on_search(self, _ = None): + if self.get_active_pane() == "player": + self._player_widget.stop() + self.set_active_pane("search") + + def _create_context_switch(self): + + self._search_radio = new_button_with_icon("system-search") + + self._search_radio.connect("clicked", self.on_search) + + # more to come... + + + + def _create_headerbar(self, main_bar = True): + headerbar = Gtk.HeaderBar() + + fullscreen = tools.new_button_with_icon("view-fullscreen") + + headerbar.pack_end(fullscreen) + + if main_bar: + + + self._create_context_switch() + + headerbar.pack_start(self._search_radio) + + + + headerbar.set_show_close_button(True) + headerbar.props.title = "Main Window" + self.set_titlebar(headerbar) + return headerbar + + + def _create_fullscreen_bar(self): + self._revealer = Gtk.Revealer() + self._revealer.add(self._create_headerbar(False)) + self.set_valign(Gtk.Align.START) + return self._revealer + + + def _create_player_widget(self): + return player_widget.PlayerWidget() + + def _create_search_widget(self): + return search_widget.SearchWidget(self) + + def run(self): + self.connect("destroy", Gtk.main_quit) + self.show_all() + Gtk.main() + + def set_active_pane(self, name: str): + self._main_stack.set_visible_child_name(name) + + def get_active_pane(self): + return self._main_stack.get_visible_child_name() + + def start_player(self, uri: str): + if self.get_active_pane() != "player": + self.set_active_pane("player") + self._player_widget.play_from_uri(uri) \ No newline at end of file diff --git a/src/gtk_mediathek_player/mediathek_request.py b/src/gtk_mediathek_player/mediathek_request.py new file mode 100644 index 0000000..e79ed74 --- /dev/null +++ b/src/gtk_mediathek_player/mediathek_request.py @@ -0,0 +1,185 @@ +import requests +import json + + +class MediathekViewWebAnswer(object): + def __init__(self, answer_json: str): + + def get_field(field: str): + if field in answer_json: + return answer_json[field] + return None + + self._raw = answer_json + + self._channel = get_field('channel') + self._topic = get_field('topic') + self._title = get_field('title') + self._description = get_field('description') + self._timestamp = get_field('timestamp') + self._duration = get_field('duration') + self._size = get_field('size') + self._url_website = get_field('url_website') + self._url_subtitle = get_field('url_subtitle') + self._filmlisteTimestamp = get_field('filmlisteTimestamp') + self._id = get_field('id') + self._url_video = get_field('url_video') + self._url_video_low = get_field('url_video_low') + self._url_video_hd = get_field('url_video_hd') + + def get_best_vid_url(self) -> str: + if self._url_video_hd is not None: + return self._url_video_hd + + if self._url_video is not None: + return self._url_video + + if self._url_video_low is not None: + return self._url_video_low + + return None + + def get_lowest_vid_url(self) -> str: + if self._url_video_low is not None: + return self._url_video_low + + if self._url_video is not None: + return self._url_video + + if self._url_video_hd is not None: + return self._url_video_hd + + return None + + def get_id(self): + return self._id + + def get_site(self): + return self._url_website + + def get_title(self): + return self._title + + def get_channel(self): + return self._channel + + def get_topic(self): + return self._topic + + def get_description(self): + return self._description + + +class MediathekViewWebRequest(object): + @staticmethod + def from_serialization( + d: dict, + request_url="https://mediathekviewweb.de/api/query" + ) -> 'MediathekViewWebRequest': + + json_query = d['queries'][0] + sort_by = d['sortBy'] + sort_order = d['sortOrder'] + future = d['future'] + offset = d['offset'] + size = d['size'] + + query = json_query['query'] + fields = json_query['fields'] + + search_channel = "channel" in fields + search_topic = "topic" in fields + search_title = "title" in fields + + return MediathekViewWebRequest( + query=query, + search_title=search_title, + search_channel=search_channel, + search_topic=search_topic, + sort_by=sort_by, + sort_order=sort_order, + future=future, + size=size, + offset=offset, + request_url=request_url + ) + + @staticmethod + def from_json( + js: str, + request_url="https://mediathekviewweb.de/api/query" + ) -> 'MediathekViewWebRequest': + return MediathekViewWebRequest.from_serialization( + d=json.loads(js), + request_url=request_url + ) + + def __init__(self, + query: str, + search_title: bool = True, + search_channel: bool = False, + search_topic: bool = False, + sort_by: str = 'timestamp', + sort_order: str = 'desc', + future: bool = False, + size: int = 10, + offset: int = 0, + request_url="https://mediathekviewweb.de/api/query"): + + assert search_title or search_channel or search_topic + assert sort_order == 'desc' or sort_order == 'asc' + + self._query = query + self._fields = [] + if search_title: + self._fields.append("title") + if search_channel: + self._fields.append("channel") + if search_topic: + self._fields.append("topic") + + self._sort_by = sort_by + self._sort_order = sort_order + self._future = future + self._size = size + self._offset = offset + self._request_url = request_url + + def serialize(self): + query = { + 'queries': [ + { + 'fields': self._fields, + 'query': self._query + } + ], + + 'sortBy': self._sort_by, + 'sortOrder': self._sort_order, + 'future': self._future, + 'offset': self._offset, + 'size': self._size + } + return query + + def to_json(self): + return json.dumps(self.serialize()) + + def perform_request(self): + headers = {'Content-type': 'text/plain'} + req = requests.post(self._request_url, + data=self.to_json(), headers=headers) + + if not req.ok: + return None + + answer = json.loads(req.text) + + if 'result' in answer: + result = answer['result'] + + if 'results' in result: + results = result['results'] + return [MediathekViewWebAnswer(i) for i in results] + + return None diff --git a/src/gtk_mediathek_player/player_widget.py b/src/gtk_mediathek_player/player_widget.py new file mode 100644 index 0000000..0e802f2 --- /dev/null +++ b/src/gtk_mediathek_player/player_widget.py @@ -0,0 +1,112 @@ +from . import tools +from . import gst_widget +from gi.repository import Gtk, GLib, Gst +import gi + +gi.require_version("Gtk", "3.0") +gi.require_version('Gst', '1.0') + + +class PlayerWidget(Gtk.Overlay): + @staticmethod + def create_player_for_uri(uri:str): + widget = PlayerWidget() + widget.play_from_uri(uri) + return widget + + def __init__(self) -> None: + Gtk.Overlay.__init__(self) + self._videoarea = gst_widget.GstWidget() + + self._create_controls() + + self.add(self._videoarea) + + self.add_overlay(self._controls) + + self._duration = None + + self._is_update = False + + GLib.timeout_add_seconds(1, self.update_controls) + + self._slider.connect("value-changed", self._on_user_slider_change) + + def update_controls(self): + + self._is_update = True + + state = self.get_state() + + if state == Gst.State.PLAYING or state == Gst.State.PAUSED: + self._duration = self._videoarea.get_duration() + + self._slider.set_range(0, self._duration) + + if self._duration is not None: + + self._slider.set_value(self._videoarea.get_position()) + + self._is_update = False + + return True + + def _on_user_slider_change(self, range): + if self._is_update: + return + value = self._slider.get_value() + self._videoarea.set_position(value) + + def _create_controls(self) -> Gtk.Box: + self._slider = Gtk.Scale.new_with_range(orientation=Gtk.Orientation.HORIZONTAL, + min=0, + max=100, + step=1) + + self._slider.props.draw_value = False + + self._play_button = tools.new_button_with_icon('media-playback-start') + self._stop_button = tools.new_button_with_icon('media-playback-stop') + + self._controls = Gtk.Box(orientation=Gtk.Orientation.HORIZONTAL) + + self._controls.pack_start(self._play_button, + expand=False, + fill=False, + padding=0) + + self._controls.pack_start(self._stop_button, + expand=False, + fill=False, + padding=0) + + self._controls.pack_end(self._slider, + expand=True, + fill=True, + padding=0) + + self._controls.set_valign(Gtk.Align.END) + + self._play_button.connect("clicked", self.play) + self._stop_button.connect("clicked", self.pause) + + def play_from_uri(self, uri: str): + self._videoarea.load_from_uri(uri) + self.play() + + def play(self, _=None): + if self.get_state() != Gst.State.PLAYING: + self._videoarea.play() + + def stop(self, _=None): + state = self.get_state() + if state == Gst.State.PLAYING or state == Gst.State.PAUSED: + self._videoarea.stop() + self._duration = None + + def pause(self, _=None): + if self.get_state() == Gst.State.PLAYING: + self._videoarea.pause() + + def get_state(self): + return self._videoarea.get_state() diff --git a/src/gtk_mediathek_player/search_widget.py b/src/gtk_mediathek_player/search_widget.py new file mode 100644 index 0000000..fdc1ef2 --- /dev/null +++ b/src/gtk_mediathek_player/search_widget.py @@ -0,0 +1,129 @@ +from gi.repository import Gtk, GLib +import gi + +gi.require_version("Gtk", "3.0") + +from . import tools +from . import mediathek_request as mr + +class SearchWidget(Gtk.Box): + def __init__(self, parent: 'main_window.MainApp') -> None: + Gtk.Box.__init__(self, orientation = Gtk.Orientation.VERTICAL) + + self._search_bar = Gtk.SearchBar() + self._search_entry = Gtk.SearchEntry() + self._search_bar.add(self._search_entry) + self.pack_start(self._search_bar, False, False, 0) + + self._search_bar.show_now() + self._search_bar.show_all() + self._search_bar.set_search_mode(True) + + self._search_entry.connect("activate", self.on_search) + + self._main_window = parent + self._current_search_cards = [] + self._search_card_container = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + + self.pack_end(self._search_card_container, False, True, 10) + + def dialog_response(self, widget, response_id): + # if the button clicked gives response OK (-5) + if response_id == Gtk.ResponseType.OK: + pass + # if the messagedialog is destroyed (by pressing ESC) + elif response_id == Gtk.ResponseType.DELETE_EVENT: + pass + widget.destroy() + + def display_warning(self, message): + messagedialog = Gtk.MessageDialog(parent=self, + flags=Gtk.DialogFlags.MODAL, + type=Gtk.MessageType.WARNING, + buttons=Gtk.ButtonsType.OK, + message_format=message) + # connect the response (of the button clicked) to the function + # dialog_response() + messagedialog.connect("response", self.dialog_response) + # show the messagedialog + messagedialog.show() + + def create_result_card(self, answer:mr.MediathekViewWebAnswer) -> Gtk.Box: + + url = answer.get_best_vid_url() + title = answer.get_title() + description = answer.get_description() + + if len(description) > 50: + description = description[:50] + "..." + + play_button = tools.new_button_with_icon("media-playback-start") + + def on_click(_ = None): + if url is not None: + self._main_window.start_player(url) + + play_button.connect("clicked", on_click) + + title_label = Gtk.Label() + title_label.set_markup(f"{title}") + title_label.set_justify(Gtk.Justification.LEFT) + title_label.set_xalign(0) + description_label = Gtk.Label() + description_label.set_markup(description) + description_label.set_justify(Gtk.Justification.LEFT) + description_label.set_xalign(0) + + text_box = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + + text_box.pack_start(title_label, True, False, 0) + text_box.pack_start(description_label, True, False, 0) + + + + card_content = Gtk.Box(orientation = Gtk.Orientation.HORIZONTAL) + card_content.pack_start(text_box, True, True, 0) + card_content.pack_start(play_button, False, False, 0) + + card = Gtk.Box(orientation = Gtk.Orientation.VERTICAL) + card.pack_start(card_content, True, True, 0) + card.pack_end(Gtk.Separator(orientation = Gtk.Orientation.HORIZONTAL), False, False, 10) + + + + return card + + def clean_results(self): + for result in self._current_search_cards: + self._search_card_container.remove(result) + + self._current_search_cards = [] + + def on_search(self, _) -> None: + search_text = self._search_entry.get_text() + + req = mr.MediathekViewWebRequest(query=search_text) + + answers = req.perform_request() + + if answers is None: + self.display_warning("error while performing the search request") + return + + if len(answers) == 0: + self.display_warning("no results found") + return + + self.clean_results() + for answer in answers: + #print(answer) + card = self.create_result_card(answer) + self._current_search_cards.append(card) + self._search_card_container.pack_start(card, False, False, 0) + + self.show_all() + + + + + diff --git a/src/gtk_mediathek_player/tools.py b/src/gtk_mediathek_player/tools.py new file mode 100644 index 0000000..a7c3303 --- /dev/null +++ b/src/gtk_mediathek_player/tools.py @@ -0,0 +1,19 @@ +from gi.repository import Gtk + + +def new_button_with_icon(freedesktop_icon: str) -> Gtk.Button: + button = Gtk.Button() + icon = Gtk.Image.new_from_icon_name(freedesktop_icon, Gtk.IconSize.BUTTON) + button.props.image = icon + return button + + +def new_radio_button_with_icon(freedesktop_icon: str, + ref_widget=None) -> Gtk.RadioButton: + if ref_widget is None: + button = Gtk.RadioButton() + else: + button = Gtk.RadioButton.new_from_widget(ref_widget) + icon = Gtk.Image.new_from_icon_name(freedesktop_icon, Gtk.IconSize.LARGE_TOOLBAR) + button.props.image = icon + return button diff --git a/testing/testing.ipynb b/testing/testing.ipynb new file mode 100644 index 0000000..309eb3b --- /dev/null +++ b/testing/testing.ipynb @@ -0,0 +1,232 @@ +{ + "metadata": { + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.5-final" + }, + "orig_nbformat": 2, + "kernelspec": { + "name": "Python 3.8.5 64-bit ('mediathek-viewer': pipenv)", + "display_name": "Python 3.8.5 64-bit ('mediathek-viewer': pipenv)", + "metadata": { + "interpreter": { + "hash": "263d32d413daecf6c7bf0192414d6289b163e9f1c6898c3837ddf25533151cb9" + } + } + } + }, + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "output_type": "stream", + "name": "stdout", + "text": [ + "/home/jonas/Dokumente/gitRepos/mediathek-viewer/src\n" + ] + } + ], + "source": [ + "cd ../src" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "import gi\n", + "from gtk_mediathek_player import main_window as mw\n", + "from gi.repository import Gtk, Gio, Gst\n", + "\n", + "gi.require_version('Gtk', '3.0')\n", + "gi.require_version('Gst', '1.0')\n", + "\n", + "Gst.init(None)\n", + "Gst.init_check(None)\n", + "\n", + "win = mw.MainApp()\n", + "#Gtk.Window.fullscreen(win)\n", + "win.run()\n", + "\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "" + ] + }, + "metadata": {}, + "execution_count": 3 + } + ], + "source": [ + "Gtk.Separator(orientation = Gtk.Orientation.HORIZONTAL)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "Gtk.Align." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "bar = Gtk.SearchBar()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bar." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "''" + ] + }, + "metadata": {}, + "execution_count": 7 + } + ], + "source": [ + "search_bar.get_text()" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "gtkglsink = Gst.ElementFactory.make(\"gtkglsink\")\n", + "glsinkbin = Gst.ElementFactory.make(\"glsinkbin\")" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "glsinkbin.set_property(\"sink\", gtkglsink)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "__gi__.GstGtkGLSink" + ] + }, + "metadata": {}, + "execution_count": 13 + } + ], + "source": [ + "type(gtkglsink)" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "<__gi__.GstGtkGLSink object at 0x7f2e5f1b1900 (GstGtkGLSink at 0x2686190)>" + ] + }, + "metadata": {}, + "execution_count": 14 + } + ], + "source": [ + "gtkglsink" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "player = Gst.ElementFactory.make(\"playbin\", \"player\")" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "output_type": "execute_result", + "data": { + "text/plain": [ + "<__gi__.GstPlayBin object at 0x7f0b3559cfc0 (GstPlayBin at 0x37fe8f0)>" + ] + }, + "metadata": {}, + "execution_count": 8 + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "Gst.State.NU" + ] + } + ] +} \ No newline at end of file