Verified Commit f4aa1b29 authored by Todd Weaver's avatar Todd Weaver
Browse files

Adding gstreamer and initial video playback results

parent a3f0d07e
......@@ -14,7 +14,10 @@
version="1.1"
id="SVGRoot"
sodipodi:docname="sm.puri.Stream.svg"
inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
inkscape:version="1.0.2 (e86c870879, 2021-01-15)"
inkscape:export-filename="/home/todd/Pictures/sm.puri.Stream.png"
inkscape:export-xdpi="96"
inkscape:export-ydpi="96">
<defs
id="defs2253">
<linearGradient
......@@ -92,7 +95,7 @@
x2="116"
y1="254"
x1="11.999994"
gradientTransform="matrix(1.0769231,0,0,1.0666667,-4.9230886,-193.93336)"
gradientTransform="matrix(1.0769231,0,0,1.0666667,-4.9230886,-197.93336)"
gradientUnits="userSpaceOnUse"
id="linearGradient1820"
xlink:href="#linearGradient4414"
......@@ -144,7 +147,7 @@
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="73.216808"
inkscape:cy="53.283693"
inkscape:cy="60.42655"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:document-rotation="0"
......@@ -182,7 +185,7 @@
width="112.00001"
height="64"
x="7.9999924"
y="45" />
y="41" />
<rect
style="display:inline;fill:url(#linearGradient5364);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;enable-background:new"
id="rect1460"
......
This diff is collapsed.
......@@ -10,8 +10,9 @@
"--socket=fallback-x11",
"--socket=wayland",
"--socket=pulseaudio",
"--filesystem=~/Videos"
"--filesystem=~/Videos",
"--talk-name=org.gtk.vfs",
"--talk-name=org.gtk.vfs.*"
],
"cleanup" : [
"/include",
......
......@@ -30,6 +30,7 @@ stream_sources = [
'main.py',
'window.py',
'results.py',
'search.py',
]
install_data(stream_sources, install_dir: moduledir)
# window.py
# results.py
#
# Copyright 2021 Todd Weaver
#
......@@ -17,18 +17,29 @@
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gtk, Gdk
gi.require_version('Gdk', '3.0')
gi.require_version('Gio', '2.0')
gi.require_version('Gst', '1.0')
gi.require_version('Handy', '1')
from gi.repository import Handy
from gi.repository import Gdk, GdkPixbuf, Gio, GLib, Gst, Gtk, Handy
Gst.init()
Gst.init_check()
Handy.init()
@Gtk.Template(resource_path='/sm/puri/Stream/ui/results.ui')
class ResultsBox(Gtk.Box):
__gtype_name__ = 'ResultsBox'
player_box = Gtk.Template.Child()
poster_image = Gtk.Template.Child()
play = Gtk.Template.Child()
pause = Gtk.Template.Child()
slider = Gtk.Template.Child()
title = Gtk.Template.Child()
channel = Gtk.Template.Child()
duration = Gtk.Template.Child()
def __init__(self, **kwargs):
super().__init__(**kwargs)
......@@ -40,6 +51,80 @@ class ResultsBox(Gtk.Box):
Gdk.Screen.get_default(), provider,
Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION)
# init gstreamer player
self.player = Gst.ElementFactory.make("playbin", "player")
self.sink = Gst.ElementFactory.make("gtksink")
video_widget = self.sink.get_property("widget")
video_widget.set_size_request(332, 186)
self.player_box.add(video_widget)
def get_duration(self, seconds):
m, s = divmod(seconds, 60)
h, m = divmod(m, 60)
if seconds >= 3600:
self.video_duration = f"{h:d}:{m:02d}:{s:02d}"
else:
self.video_duration = f"{m:d}:{s:02d}"
def on_poster_load(self, source, async_res, user_data):
self.poster_image.clear()
pixbuf = GdkPixbuf.Pixbuf.new_from_stream_finish(async_res)
self.poster_image.set_from_pixbuf(pixbuf)
def setup_stream(self, video_meta):
video_title = video_meta['title']
video_channel = video_meta['author']
video_id = video_meta['videoId']
poster_uri = video_meta['poster_uri']
self.get_duration(video_meta['lengthSeconds'])
self.title.set_label(video_title)
self.channel.set_label(video_channel)
self.duration.set_label(self.video_duration)
uri = f"http://iteroni.com/latest_version?id={video_id}&itag=18"
# this should be done on play button press
self.player.set_property("uri", uri)
self.player.set_property("video-sink", self.sink)
poster_file = Gio.File.new_for_uri(poster_uri)
result = GdkPixbuf.Pixbuf.new_from_stream_at_scale_async(poster_file.read(),
332, 186, # width and height
True, # preserve_aspect_ratio
None, # cancellable
self.on_poster_load, # callback,
video_id) # user_data
def update_slider(self):
if not self.is_playing:
return False
else:
success, self.duration = self.player.query_duration(Gst.Format.TIME)
# GtkScale is set to 100%, calculate duration and position steps
self.percent = 100 / (self.duration / Gst.SECOND)
# get current position (nanoseconds)
success, position = self.player.query_position(Gst.Format.TIME)
position_value = float(position) / Gst.SECOND * self.percent
# is negative number when not successful, so put it to 0
if not success:
position_value = 0
# block seek slider function so it doesn't loop itself
self.slider.handler_block_by_func(self.seek_slider)
self.slider.set_value(position_value)
self.slider.handler_unblock_by_func(self.seek_slider)
return True
@Gtk.Template.Callback()
def audio_dl_button(self, button):
print("audio_dl_button")
......@@ -52,13 +137,27 @@ class ResultsBox(Gtk.Box):
def play_button(self, button):
self.play.set_visible(False)
self.pause.set_visible(True)
print("play_button")
self.player.set_state(Gst.State.PLAYING)
self.is_playing = True
# hide the poster, show the video
self.player_box.show_all()
self.poster_image.hide()
# initialize the slider
self.update_slider()
# allow seeking
self.slider.set_sensitive(True)
# update slider to track video time in slider
GLib.timeout_add(1000, self.update_slider)
@Gtk.Template.Callback()
def pause_button(self, button):
self.play.set_visible(True)
self.pause.set_visible(False)
print("pause_button")
self.player.set_state(Gst.State.PAUSED)
self.is_playing = False
@Gtk.Template.Callback()
def speed_button(self, button):
......@@ -67,3 +166,12 @@ class ResultsBox(Gtk.Box):
@Gtk.Template.Callback()
def fullscreen_button(self, button):
print("fullscreen_button")
@Gtk.Template.Callback()
def seek_slider(self, scale):
seek = scale.get_value()
# allow seeking when playing
self.player.seek_simple(Gst.Format.TIME,
Gst.SeekFlags.FLUSH | Gst.SeekFlags.KEY_UNIT,
seek * Gst.SECOND / self.percent)
# search.py
#
# Copyright 2021 Todd Weaver
#
# This program 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.
#
# This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
from socket import timeout
from urllib.parse import urlencode
from gi.repository import GLib
import urllib.request
import json
class Search:
def __init__(self, **kwargs):
self.query = kwargs.get('query', None)
self.headers = ({
"User-Agent":
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
"AppleWebKit/537.36 (KHTML, like Gecko) Chrome"
"/75.0.3770.100 Safari/537.36"
})
self.do_search()
def get_strong_instance(self):
# lookup instances, get a strong one, return it
self.instance = "https://invidious.xyz"
def do_search(self):
self.get_strong_instance()
enc_query = urlencode({'q': self.query})
uri = f"{self.instance}/api/v1/search?{enc_query}"
url = urllib.request.Request(uri, headers=self.headers)
try:
results = urllib.request.urlopen(url, timeout=5)
except (urllib.error.HTTPError, timeout):
print("URL did not respond")
try:
self.json = json.loads(results.read())
except UnboundLocalError:
print("json did not load from url results")
self.get_poster_url()
def get_poster_url(self):
# tweak json with local poster url
for video_meta in self.json:
for poster in video_meta['videoThumbnails']:
if poster['quality'] == 'medium':
video_meta['poster_uri'] = poster['url']
......@@ -4,7 +4,6 @@
<file>ui/window.ui</file>
<file>ui/results.ui</file>
<file>ui/results.css</file>
<file>ui/video-placeholder-332x186.png</file>
<file alias="gtk/help-overlay.ui">ui/help-overlay.ui</file>
</gresource>
</gresources>
.results {
border: 1px solid gray;
margin: 6px;
margin: 0px;
padding: 6px;
padding-bottom: 12px;
border-radius: 8px;
......@@ -9,10 +9,6 @@
background-color: white;
}
.videoframe {
border: 1px solid black;
}
button.rounded-button {
padding: 4px;
border-radius: 9999px;
......
......@@ -2,7 +2,7 @@
<!-- Generated with glade 3.38.2 -->
<interface>
<requires lib="gtk+" version="3.20"/>
<object class="GtkAdjustment" id="duration">
<object class="GtkAdjustment" id="slider-duration">
<property name="upper">100</property>
<property name="step-increment">1</property>
<property name="page-increment">10</property>
......@@ -12,8 +12,8 @@
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">start</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="margin-start">4</property>
<property name="margin-end">4</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="orientation">vertical</property>
......@@ -26,19 +26,30 @@
<property name="halign">center</property>
<property name="valign">center</property>
<child>
<object class="GtkImage">
<property name="width-request">322</property>
<object class="GtkBox" id="player_box">
<property name="width-request">332</property>
<property name="height-request">186</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">center</property>
<property name="valign">center</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="resource">/sm/puri/Stream/ui/video-placeholder-332x186.png</property>
<style>
<class name="videoframe"/>
</style>
<property name="hexpand">False</property>
<property name="vexpand">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkImage" id="poster_image">
<property name="width-request">332</property>
<property name="height-request">186</property>
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="icon-name">video-x-generic</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="index">-1</property>
......@@ -275,15 +286,18 @@
</packing>
</child>
<child>
<object class="GtkScale">
<object class="GtkScale" id="slider">
<property name="visible">True</property>
<property name="sensitive">False</property>
<property name="can-focus">True</property>
<property name="margin-top">6</property>
<property name="margin-bottom">6</property>
<property name="adjustment">duration</property>
<property name="adjustment">slider-duration</property>
<property name="show-fill-level">True</property>
<property name="round-digits">1</property>
<property name="draw-value">False</property>
<property name="has-origin">False</property>
<signal name="value-changed" handler="seek_slider" swapped="no"/>
</object>
<packing>
<property name="expand">False</property>
......@@ -304,26 +318,40 @@
<object class="GtkBox" id="details">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="margin-start">6</property>
<property name="margin-end">6</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox">
<object class="GtkLabel" id="title">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="margin-start">6</property>
<property name="hexpand">True</property>
<property name="orientation">vertical</property>
<property name="label" translatable="yes">Title</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkBox">
<property name="visible">True</property>
<property name="can-focus">False</property>
<child>
<object class="GtkLabel" id="title">
<object class="GtkLabel" id="channel">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Title</property>
<property name="label" translatable="yes">Channel</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
<attribute name="scale" value="1.2"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
......@@ -332,13 +360,16 @@
</packing>
</child>
<child>
<object class="GtkLabel" id="channel">
<object class="GtkLabel" id="duration">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">start</property>
<property name="hexpand">True</property>
<property name="label" translatable="yes">Channel</property>
<property name="halign">end</property>
<property name="label" translatable="yes">0:00</property>
<property name="justify">right</property>
<property name="ellipsize">end</property>
<attributes>
<attribute name="weight" value="bold"/>
</attributes>
</object>
<packing>
<property name="expand">False</property>
......@@ -347,22 +378,6 @@
</packing>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkLabel" id="duratio">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="halign">end</property>
<property name="valign">center</property>
<property name="margin-end">6</property>
<property name="label" translatable="yes">0:00</property>
<property name="justify">right</property>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">True</property>
......
......@@ -4,13 +4,13 @@
<requires lib="gtk+" version="3.20"/>
<requires lib="libhandy" version="1.2"/>
<template class="StreamWindow" parent="HdyApplicationWindow">
<property name="width-request">350</property>
<property name="height-request">480</property>
<property name="width-request">360</property>
<property name="height-request">720</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="hexpand">False</property>
<property name="vexpand">True</property>
<property name="default-width">780</property>
<property name="default-height">480</property>
<property name="default-width">360</property>
<property name="default-height">720</property>
<child>
<object class="GtkBox">
<property name="visible">True</property>
......@@ -23,13 +23,14 @@
<property name="title" translatable="yes">Stream</property>
<property name="show-close-button">True</property>
<child>
<object class="GtkToggleButton" id="search_button">
<object class="GtkToggleButton" id="search_bar_toggle">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="receives-default">False</property>
<property name="tooltip-text" translatable="yes">Search</property>
<property name="valign">center</property>
<property name="active">True</property>
<signal name="clicked" handler="search_toggle" swapped="no"/>
<child>
<object class="GtkImage">
<property name="visible">True</property>
......@@ -81,13 +82,14 @@
<property name="can-focus">False</property>
<property name="search-mode-enabled">True</property>
<child>
<object class="GtkSearchEntry" id="search_entry">
<object class="GtkSearchEntry" id="search_entry_box">
<property name="visible">True</property>
<property name="can-focus">True</property>
<property name="has-focus">True</property>
<property name="primary-icon-name">edit-find-symbolic</property>
<property name="primary-icon-activatable">False</property>
<property name="primary-icon-sensitive">False</property>
<signal name="search-changed" handler="search_entry" swapped="no"/>
</object>
</child>
</object>
......@@ -105,7 +107,7 @@
<object class="GtkStack" id="stack">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="hexpand">False</property>
<property name="vexpand">True</property>
<child>
<object class="HdyStatusPage" id="status_page">
......@@ -119,18 +121,20 @@
<child>
<object class="GtkScrolledWindow" id="results_window">
<property name="can-focus">False</property>
<property name="hexpand">True</property>
<property name="hexpand">False</property>
<property name="vexpand">True</property>
<property name="hscrollbar-policy">never</property>
<child>
<object class="GtkFlowBox" id="results_list">
<property name="can-focus">False</property>
<property name="visible">True</property>
<property name="hexpand">True</property>
<property name="homogeneous">True</property>
<property name="max-children-per-line">5</property>
<property name="hexpand">False</property>
<property name="max-children-per-line">1</property>
<property name="selection-mode">none</property>
<property name="activate-on-single-click">False</property>
<child>
<placeholder/>
</child>
</object>
</child>
</object>
......
......@@ -15,55 +15,56 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import logging
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import GLib, Gtk
gi.require_version('Handy', '1')
from gi.repository import Handy
from gi.repository import Gtk, Handy
Handy.init()
from .results import ResultsBox
from .search import Search
@Gtk.Template(resource_path='/sm/puri/Stream/ui/window.ui')
class StreamWindow(Handy.ApplicationWindow):
__gtype_name__ = 'StreamWindow'
search_button = Gtk.Template.Child()
search_bar_toggle = Gtk.Template.Child()
search_bar = Gtk.Template.Child()
search_entry = Gtk.Template.Child()
status_page = Gtk.Template.Child()
results_window = Gtk.Template.Child()
results_list = Gtk.Template.Child()
def search_button_cb(self, widget):
@Gtk.Template.Callback()
def search_toggle(self, widget):
# toggle the True/False from what is current
self.search_bar.set_visible(self.search_button.get_active())
self.search_bar.set_visible(self.search_bar_toggle.get_active())
def search_entry_cb(self, widget, entry):
entry_text = entry.get_text()
print("Text entry: " + entry_text)
def clear_entries(self):
children = self.results_list.get_children()
for child in children:
child.destroy()
@Gtk.Template.Callback()
def search_entry(self, widget):