pax_global_header00006660000000000000000000000064145750527400014523gustar00rootroot0000000000000052 comment=2995b99d079b9310d794eaeee8c2e849ed69f31f livi-v0.1.0/000077500000000000000000000000001457505274000126525ustar00rootroot00000000000000livi-v0.1.0/.dir-locals.el000066400000000000000000000010041457505274000152760ustar00rootroot00000000000000( (c-mode . ( (c-file-style . "linux") (indent-tabs-mode . nil) (c-basic-offset . 2) )) (setq auto-mode-alist (cons '("\\.ui$" . nxml-mode) auto-mode-alist)) (nxml-mode . ( (indent-tabs-mode . nil) )) (css-mode . ( (css-indent-offset . 2) )) (js-mode . ( (indent-tabs-mode . nil) (js-indent-level . 2) )) (meson-mode . ( (indent-tabs-mode . nil) )) ) livi-v0.1.0/.editorconfig000066400000000000000000000006021457505274000153250ustar00rootroot00000000000000root = true [*] charset = utf-8 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [meson.build] indent_size = 2 tab_size = 2 indent_style = space [*.{c,h,c.in,h.in}] indent_size = 2 tab_size = 2 indent_style = space max_line_length = 80 [*.css] indent_size = 2 tab_size = 2 indent_style = space [*.xml] indent_size = 2 tab_size = 2 indent_style = space livi-v0.1.0/.gitignore000066400000000000000000000003631457505274000146440ustar00rootroot00000000000000.flatpak-builder/ _build TAGS tags vgdump *.swp *~ \#*# .\#* .vscode/ *.gcov debian/.debhelper/ debian/debhelper-build-stamp debian/files debian/livi.debhelper.log debian/livi.substvars debian/livi/ po/livi.pot po/messages.mo subprojects/gtk/ livi-v0.1.0/.gitlab-ci.yml000066400000000000000000000054021457505274000153070ustar00rootroot00000000000000include: - project: 'guidog/meta-phosh' ref: '04a85df2f3311f3012b6c49bf353b376ae0f32b2' file: '/ci/phosh-common-jobs.yml' - remote: 'https://gitlab.freedesktop.org/freedesktop/ci-templates/-/raw/34039cd573a2df832d465bc9e4c5f543571f5241/templates/ci-fairy.yml' - project: 'gnome/citemplates' file: 'flatpak/flatpak-ci-initiative-sdk-extensions.yml' stages: - build - style-checks - test+docs - packaging workflow: rules: - if: $CI_PIPELINE_SOURCE == 'merge_request_event' # Don't trigger a branch pipeline if there is an open MR - if: $CI_COMMIT_BRANCH && $CI_OPEN_MERGE_REQUESTS when: never - if: $CI_COMMIT_TAG - if: $CI_COMMIT_BRANCH default: # Protect CI infra from rogue jobs timeout: 15 minutes # Allow jobs to be caneled on new commits interruptible: true # Retry on infra hickups automatically retry: max: 1 when: - 'api_failure' - 'runner_system_failure' - 'scheduler_failure' - 'stuck_or_timeout_failure' variables: # For ci-fairy FDO_UPSTREAM_REPO: guidog/livi DEBIAN_IMAGE: debian:trixie COMMON_BUILD_OPTS: .trixie_vars: &trixie_vars variables: DIST: trixie BUILD_OPTS: ${COMMON_BUILD_OPTS} .build_step: &build_step script: - 'echo "Build opts: ${BUILD_OPTS}"' - meson setup ${BUILD_OPTS} _build - meson compile -C _build - meson test -C _build --print-errorlogs # Sanity checks of MR settings and commit logs sanity: extends: - .fdo.ci-fairy stage: style-checks stage: style-checks variables: GIT_DEPTH: "100" needs: [] script: | ci-fairy check-commits --junit-xml=commit-message-junit-report.xml artifacts: reports: junit: commit-message-junit-report.xml rules: - if: '$CI_PIPELINE_SOURCE == "merge_request_event" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME == $CI_DEFAULT_BRANCH' check-po: needs: [] stage: test+docs extends: .phosh-check-po build:native-debian-trixie: needs: [] stage: build image: ${DEBIAN_IMAGE} before_script: - "sed -i 's/Types: deb/Types: deb deb-src/' /etc/apt/sources.list.d/debian.sources" - apt-get -y update - apt-get -y install eatmydata - eatmydata apt-get -y --no-install-recommends install git glslc ca-certificates - eatmydata apt-get -y --no-install-recommends build-dep libgtk-4-dev - eatmydata apt-get -y --no-install-recommends build-dep . <<: *trixie_vars <<: *build_step artifacts: paths: - _build flatpak:master: needs: [] extends: '.flatpak' stage: 'packaging' allow_failure: true variables: APP_ID: "org.sigxcpu.Livi.Devel" BUNDLE: "org.sigxcpu.Livi.Devel.flatpak" FLATPAK_MODULE: "livi" MANIFEST_PATH: "org.sigxcpu.Livi.json" RUNTIME_REPO: "https://nightly.gnome.org/gnome-nightly.flatpakrepo" livi-v0.1.0/.gitlab-ci/000077500000000000000000000000001457505274000145635ustar00rootroot00000000000000livi-v0.1.0/.gitlab-ci/check-po000077500000000000000000000007361457505274000162100ustar00rootroot00000000000000#!/bin/bash cd po/ || exit 1 # barf on untranslated C files. Seems intltool # can't be told to exit with non-zero exit status # in this case if intltool-update -m 2>&1 | grep -E -qs '/.*\.(c|ui)'; then intltool-update -m exit 1 fi # Check for broken po files for file in *.po; do echo -n "Checking ${file}: " msgfmt -v -c "${file}" # Check for errors, msgfmt returns 0 on errors too if msgfmt -c "${file}" 2>&1 | grep -qs 'fatal error'; then exit 1 fi done livi-v0.1.0/.gitlab-ci/check-style.py000077500000000000000000000124731457505274000173620ustar00rootroot00000000000000#!/bin/env python3 # # Based on check-style.py by # Carlos Garnacho import argparse import os import re import subprocess import sys import tempfile # Path relative to this script uncrustify_cfg = ".gitlab-ci/uncrustify.cfg" def run_diff(sha): proc = subprocess.run( ["git", "diff", "-U0", "--function-context", sha, "HEAD"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", ) return proc.stdout.strip().splitlines() def find_chunks(diff): file_entry_re = re.compile(r"^\+\+\+ b/(.*)$") diff_chunk_re = re.compile(r"^@@ -\d+,\d+ \+(\d+),(\d+)") file = None chunks = [] for line in diff: match = file_entry_re.match(line) if match: file = match.group(1) match = diff_chunk_re.match(line) if match: start = int(match.group(1)) len = int(match.group(2)) end = start + len if len > 0 and ( file.endswith(".c") or file.endswith(".h") or file.endswith(".vala") ): chunks.append({"file": file, "start": start, "end": end}) return chunks def reformat_chunks(chunks, rewrite, dry_run): # Creates temp file with INDENT-ON/OFF comments def create_temp_file(file, start, end): with open(file) as f: tmp = tempfile.NamedTemporaryFile() if start > 1: tmp.write(b"/** *INDENT-OFF* **/\n") for i, line in enumerate(f, start=1): if i == start - 1: tmp.write(b"/** *INDENT-ON* **/\n") tmp.write(bytes(line, "utf-8")) if i == end - 1: tmp.write(b"/** *INDENT-OFF* **/\n") tmp.seek(0) return tmp # Removes uncrustify INDENT-ON/OFF helper comments def remove_indent_comments(output): tmp = tempfile.NamedTemporaryFile() for line in output: if line != b"/** *INDENT-OFF* **/\n" and line != b"/** *INDENT-ON* **/\n": tmp.write(line) tmp.seek(0) return tmp changed = None for chunk in chunks: # Add INDENT-ON/OFF comments tmp = create_temp_file(chunk["file"], chunk["start"], chunk["end"]) # uncrustify chunk proc = subprocess.run( ["uncrustify", "-c", uncrustify_cfg, "-f", tmp.name], stdout=subprocess.PIPE, ) reindented = proc.stdout.splitlines(keepends=True) if proc.returncode != 0: continue tmp.close() # Remove INDENT-ON/OFF comments formatted = remove_indent_comments(reindented) if dry_run is True: # Show changes proc = subprocess.run( ["diff", "-up", "--color=always", chunk["file"], formatted.name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", ) diff = proc.stdout if diff != "": output = re.sub("\t", "↦\t", diff) print(output) changed = True else: # Apply changes diff = subprocess.run( ["diff", "-up", chunk["file"], formatted.name], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, ) subprocess.run(["patch", chunk["file"]], input=diff.stdout) formatted.close() return changed def main(argv): parser = argparse.ArgumentParser( description="Check code style. Needs uncrustify installed." ) parser.add_argument( "--sha", metavar="SHA", type=str, help="SHA for the commit to compare HEAD with" ) parser.add_argument( "--dry-run", "-d", type=bool, action=argparse.BooleanOptionalAction, help="Only print changes to stdout, do not change code", ) parser.add_argument( "--rewrite", "-r", type=bool, action=argparse.BooleanOptionalAction, help="Whether to amend the result to the last commit (e.g. 'git rebase --exec \"%(prog)s -r\"')", ) if not os.path.exists(".git"): print("Not in toplevel of a git repository", fille=sys.stderr) return 1 args = parser.parse_args(argv) sha = args.sha or "HEAD^" diff = run_diff(sha) chunks = find_chunks(diff) changed = reformat_chunks(chunks, args.rewrite, args.dry_run) if args.dry_run is not True and args.rewrite is True: proc = subprocess.run(["git", "add", "-p"]) if proc.returncode == 0: # Commit the added changes as a squash commit subprocess.run( ["git", "commit", "--squash", "HEAD", "-C", "HEAD"], stdout=subprocess.DEVNULL, ) # Delete the unapplied changes subprocess.run(["git", "reset", "--hard"], stdout=subprocess.DEVNULL) return 0 elif args.dry_run is True and changed is True: print( f""" Issue the following commands in your local tree to apply the suggested changes: $ git rebase {sha} --exec "./.gitlab-ci/check-style.py -r" $ git rebase --autosquash {sha} Don't trust uncrustify unconditionally. """ ) return 1 return 0 if __name__ == "__main__": sys.exit(main(sys.argv[1:])) livi-v0.1.0/.gitlab-ci/commit-rules.yml000066400000000000000000000016721457505274000177340ustar00rootroot00000000000000patterns: deny: - regex: '^$CI_MERGE_REQUEST_PROJECT_URL/(-/)?merge_requests/$CI_MERGE_REQUEST_IID$' message: Commit message must not contain a link to its own merge request - regex: '^[^:]+: [a-z]' message: "Commit description in commit message subject should be properly Capitalized. E.g. 'monitor: Avoid crash on unplug'" where: subject - regex: '^\S*\.(c|h|ui):' message: Commit message subject prefix should not include .c, .h etc. where: subject - regex: '([^.]\.|[:,;])\s*$' message: Commit message subject should not end with punctuation where: subject - regex: '^[A-Z]\S*:' message: "Identifier in commit message subject should start lowercase 'monitor: Avoid crash on unplug'" where: subject require: - regex: '^[a-z0-9,\.\+\-/#=_]+:' message: "Commit message should start with a lowercase identifier 'monitor: Avoid crash on unplug'" where: subject livi-v0.1.0/.gitlab-ci/uncrustify.cfg000066400000000000000000000111171457505274000174600ustar00rootroot00000000000000# Indent by two spaces indent_columns = 2 # No tabs indent_with_tabs = 0 # Line length code_width = 100 # Whether to remove superfluous semicolons mod_remove_extra_semicolon = true # indent goto by 1 (or -1 brace level) indent_label = -1 # don't indent case after switch indent_switch_case = 0 # # Keywords and operators # # Add between 'do' and '{'. sp_do_brace_open = add # Add space between '}' and 'while'. sp_brace_close_while = add # Add 'while' and '('. sp_while_paren_open = add # Add or remove space around boolean operators '&&' and '||'. sp_bool = add # Ternary operator sp_cond_ternary_short = remove # Remove newline between 'struct and '{'. nl_struct_brace = remove # Remove newline between 'if' and '{'. nl_if_brace = remove # Remove newline between '}' and 'else'. nl_brace_else = remove # Remove newline between 'else' and '{'. nl_else_brace = remove # Remove newline between 'else' and 'if'. nl_else_if = remove # Add or remove newline between 'for' and '{'. nl_for_brace = remove # Add or remove newline between 'while' and '{'. nl_while_brace = remove # Treat iterators as for loops: set FOR wl_list_for_each wl_list_for_each_reverse wl_list_for_each_safe # Remove braces on single line if/for/while statements mod_full_brace_if = remove mod_full_brace_for = remove mod_full_brace_while = remove # If any must be braced, they are all braced. If all can be unbraced, then the braces are removed. mod_full_brace_if_chain = 1 # Remove braces around case (when there are no variables declarations) mod_case_brace = remove # Don't remove branches if the statement has more than one line mod_full_brace_nl = 2 # # Function declarations, definitions and calls # # Add space between function name and '(' on function declaration. sp_func_proto_paren = add # Add or remove space between function name and '()' on function declaration # without parameters. sp_func_proto_paren_empty = add # Add space between function name and '(' on function definition. sp_func_def_paren = add # Add or remove space between function name and '(' on function calls. sp_func_call_paren = add # Whether to force indentation of function definitions to start in column 1. indent_func_def_force_col1 = true # Add newline between return type and function name in a function definition. nl_func_type_name = add # Add newline between function signature and '{'. nl_fdef_brace = add # Whether to align variable definitions in prototypes and functions. align_func_params = true # The span for aligning function prototypes. align_func_proto_span = 8 # Add space between 'decltype(...)' and word. sp_after_decltype = add # Add or remove space after a pointer star '*', if followed by a function # prototype or function definition. sp_after_ptr_star_func = remove # Add or remove newline between a function call's ')' and '{', as in # 'list_for_each(item, &list) { }'. nl_fcall_brace = add # # Typedefs # # Add space between '}' and the name of a typedef on the same line. sp_brace_typedef = add # # Comments # # Add space after the opening of a C++ comment, i.e. '// A' vs. '//A'. sp_cmt_cpp_start = add # # Preprocessor # # Add or remove space between #else or #endif and a trailing comment. sp_endif_cmt = add # Newlines at the start and end of the file. nl_start_of_file = remove nl_end_of_file = add nl_end_of_file_min = 1 # # Variable definitions # # How to align the '*' in variable definitions. # # 0: Part of the type 'void * foo;' (default) # 1: Part of the variable 'void *foo;' # 2: Dangling 'void *foo;' # Dangling: the '*' will not be taken into account when aligning. align_var_def_star_style = 2 # Same for typedefs align_typedef_star_style = 2 # The gap for aligning struct/union member definitions. align_var_struct_gap = 1 # The span for aligning struct/union member definitions. align_var_struct_span = 8 # Remove space between pointer stars '*'. sp_between_ptr_star = remove # Add space before '(' of control statements ('if', 'for', 'switch', 'while', etc.) sp_before_sparen = add livi-v0.1.0/COPYING000066400000000000000000001045141457505274000137120ustar00rootroot00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) 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 . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . livi-v0.1.0/NEWS000066400000000000000000000031441457505274000133530ustar00rootroot00000000000000livi 0.1.0 ---------- Released March 2024 * Allow to paste URLs Pasting an uri into the main window via ctrl-v plays that video. * Start videos at last position * Show/Hide controls in more situations automatically * Detect some SDH subtitles * List mkv formats in file chooser * Detect hw accelerations on more devices/configurations * Add support for DMABuf import and graphics offload This needs GTK >= 4.14 and gstreamer 1.24 * Issues fixed: * https://gitlab.gnome.org/guidog/livi/-/issues/13 * Contributors: * Guido Günther * Robert Mader livi 0.0.6 ---------- Released January 2024 * Remove info bar at top of screen * Make bottom bar adjust to window size * Add main menu giving access to newly added file open, shortcut and about dialogs * Use vertical menu for playback speed * Move empty state indicators closer to designs * Allow for a narrow and wide layouts * Add menu for language and subtitle selection * Add visualization for audio only streams. * Autohide panels and pointer when there's a pointing device * Contributors: * Guido Günther livi 0.0.5 ---------- Released December 2023 * More user feedback on play, pause, skip, etc * Allow to process URLs via yt-dlp * Rename to "LightVideo" to avoid name confusion * Transparent title bar to have more screen estate when not fullscreen * Contributors: * Guido Günther * Krassy Boykinov livi 0.0.4 ---------- * Avoid possible crash on close * Allow to set playback speed livi 0.0.3 ---------- Released: March 2023 * Catch up with latest GTK and libadwaita * Add CI * Several bug fixes * Use black background * Detect more accelrators livi-v0.1.0/README.md000066400000000000000000000014511457505274000141320ustar00rootroot00000000000000Light Video =========== Minimalistic video player using GTK4 and GStreamer. The main purpose is to make playing hw accelerated videos with hantro and OpenGL simple. It supports: - Inhibiting suspend/idle when playing video - Stopping video playback on (i.e. power button toggled) blank - Registering as default video player in GNOME control center ![Playing video in landscape fullscreen mode](screenshots/landscape-fullscreen.png) Building ======== Flatpak build: # Intial setup flatpak install --user org.gnome.Sdk//master flatpak install --user org.gnome.Platform//master # Build flatpak-builder --force-clean --install --user _build/ org.sigxcpu.Livi.json Regular build: # Intial setup apt build-dep . # Build meson setup . _build meson compile -C _build livi-v0.1.0/build-aux/000077500000000000000000000000001457505274000145445ustar00rootroot00000000000000livi-v0.1.0/build-aux/0001-play-Emit-correct-signal.patch000066400000000000000000000022721457505274000227210ustar00rootroot00000000000000From a122f67365dc593c362d00dfc8a29f737c6e3c51 Mon Sep 17 00:00:00 2001 Message-Id: From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Fri, 9 Jul 2021 14:55:43 +0200 Subject: [PATCH] play: Emit correct signal SIGNAL_MEDIA_INFO_UPDATED should be emitted on media info changes, not SIGNAL_VIDEO_DIMENSIONS_CHANGED. --- gst-libs/gst/play/gstplay-signal-adapter.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gst-libs/gst/play/gstplay-signal-adapter.c b/gst-libs/gst/play/gstplay-signal-adapter.c index cc5f0e0ce4..cf8b57e198 100644 --- a/gst-libs/gst/play/gstplay-signal-adapter.c +++ b/gst-libs/gst/play/gstplay-signal-adapter.c @@ -168,7 +168,7 @@ gst_play_signal_adapter_emit (GstPlaySignalAdapter * self, GstPlayMediaInfo *media_info; gst_structure_get (message_data, GST_PLAY_MESSAGE_DATA_MEDIA_INFO, GST_TYPE_PLAY_MEDIA_INFO, &media_info, NULL); - g_signal_emit (self, signals[SIGNAL_VIDEO_DIMENSIONS_CHANGED], 0, + g_signal_emit (self, signals[SIGNAL_MEDIA_INFO_UPDATED], 0, media_info); g_object_unref (media_info); break; -- 2.30.2 livi-v0.1.0/build-aux/flathub/000077500000000000000000000000001457505274000161715ustar00rootroot00000000000000livi-v0.1.0/build-aux/flathub/shared-modules/000077500000000000000000000000001457505274000211055ustar00rootroot00000000000000livi-v0.1.0/build-aux/flathub/shared-modules/gudev/000077500000000000000000000000001457505274000222175ustar00rootroot00000000000000livi-v0.1.0/build-aux/flathub/shared-modules/gudev/gudev.json000066400000000000000000000012161457505274000242240ustar00rootroot00000000000000{ "name": "gudev", "buildsystem": "meson", "config-opts": [ "-Dtests=disabled", "-Dvapi=disabled", "-Dintrospection=disabled", "-Dgtk_doc=false" ], "cleanup": [ "/include", "/etc", "/libexec", "/sbin", "/lib/pkgconfig", "/lib/systemd", "/man", "/share/aclocal", "/share/doc", "/share/gtk-doc", "/share/man", "/share/pkgconfig", "*.la", "*.a" ], "sources": [ { "type": "archive", "url": "https://download.gnome.org/sources/libgudev/237/libgudev-237.tar.xz", "sha256": "0d06b21170d20c93e4f0534dbb9b0a8b4f1119ffb00b4031aaeb5b9148b686aa" } ] } livi-v0.1.0/data/000077500000000000000000000000001457505274000135635ustar00rootroot00000000000000livi-v0.1.0/data/icons/000077500000000000000000000000001457505274000146765ustar00rootroot00000000000000livi-v0.1.0/data/icons/play-large-symbolic.svg000066400000000000000000000007331457505274000212760ustar00rootroot00000000000000 livi-v0.1.0/data/icons/region-symbolic.svg000066400000000000000000000007371457505274000205300ustar00rootroot00000000000000 livi-v0.1.0/data/icons/skip-backwards-10-symbolic.svg000066400000000000000000000026471457505274000223720ustar00rootroot00000000000000 livi-v0.1.0/data/icons/skip-forward-30-symbolic.svg000066400000000000000000000036751457505274000221010ustar00rootroot00000000000000 livi-v0.1.0/data/icons/speedometer4-symbolic.svg000066400000000000000000000165661457505274000216540ustar00rootroot00000000000000 livi-v0.1.0/data/icons/view-restore-symbolic.svg000066400000000000000000000030561457505274000216750ustar00rootroot00000000000000 livi-v0.1.0/data/meson.build000066400000000000000000000025601457505274000157300ustar00rootroot00000000000000datadir = get_option('datadir') desktop_file = i18n.merge_file( input: 'org.sigxcpu.Livi.desktop.in', output: 'org.sigxcpu.Livi.desktop', type: 'desktop', po_dir: '../po', install: true, install_dir: join_paths(datadir, 'applications') ) desktop_utils = find_program('desktop-file-validate', required: false) if desktop_utils.found() test('Validate desktop file', desktop_utils, args: [desktop_file] ) endif appstream_file = i18n.merge_file( input: 'org.sigxcpu.Livi.metainfo.xml.in', output: 'org.sigxcpu.Livi.metainfo.xml', po_dir: '../po', install: true, install_dir: join_paths(get_option('datadir'), 'metainfo') ) appstream_util = find_program('appstreamcli', required: false) if appstream_util.found() test('Validate appstream file', appstream_util, args: ['validate', '--no-net', appstream_file] ) endif install_data('org.sigxcpu.Livi.gschema.xml', install_dir: join_paths(get_option('datadir'), 'glib-2.0/schemas') ) compile_schemas = find_program('glib-compile-schemas', required: false) if compile_schemas.found() test('Validate schema file', compile_schemas, args: ['--strict', '--dry-run', meson.current_source_dir()] ) endif compiled = gnome.compile_schemas( build_by_default: true ) install_data('org.sigxcpu.Livi.svg', install_dir: join_paths( datadir, 'icons', 'hicolor', 'scalable', 'apps' ) ) livi-v0.1.0/data/org.sigxcpu.Livi.desktop.in000066400000000000000000000022671457505274000207440ustar00rootroot00000000000000[Desktop Entry] Name=Light Video Keywords=Video;Movie; Comment=Play videos Exec=livi %U Terminal=false Type=Application Categories=GTK;GNOME;AudioVideo;Player;Video; StartupNotify=true MimeType=video/mp2t;video/mp4;video/mp4v-es;video/mpeg;video/mpeg-system;video/msvideo;video/ogg;video/quicktime;video/vivo;video/vnd.divx;video/vnd.mpegurl;video/vnd.rn-realvideo;video/vnd.vivo;video/webm;video/x-anim;video/x-avi;video/x-flc;video/x-fli;video/x-flic;video/x-flv;video/x-m4v;video/x-matroska;video/x-mjpeg;video/x-mpeg;video/x-mpeg2;video/x-ms-asf;video/x-ms-asf-plugin;video/x-ms-asx;video/x-msvideo;video/x-ms-wm;video/x-ms-wmv;video/x-ms-wmx;video/x-ms-wvx;video/x-nsv;video/x-ogm+ogg;video/x-theora;video/x-theora+ogg;video/x-totem-stream;audio/x-pn-realaudio;application/smil;application/smil+xml;application/x-quicktime-media-link;application/x-smil;text/google-video-pointer;x-content/video-dvd;x-scheme-handler/pnm;x-scheme-handler/mms;x-scheme-handler/net;x-scheme-handler/rtp;x-scheme-handler/rtmp;x-scheme-handler/rtsp;x-scheme-handler/mmsh;x-scheme-handler/uvox;x-scheme-handler/icy;x-scheme-handler/icyx; Icon=org.sigxcpu.Livi X-GNOME-AutoRestart=true X-Purism-FormFactor=Workstation;Mobile; livi-v0.1.0/data/org.sigxcpu.Livi.gschema.xml000066400000000000000000000014171457505274000210700ustar00rootroot00000000000000 [] Recently played videos List of recently played videos and their stream position. 50 Maximum number of videos in recent list The maximum number of recently played videos that are kept in the list of recent videos. livi-v0.1.0/data/org.sigxcpu.Livi.metainfo.xml.in000066400000000000000000000064501457505274000216720ustar00rootroot00000000000000 org.sigxcpu.Livi Light Video CC0-1.0 GPL-3.0-or-later

A minimalistic GTK4 and gstreamer based video player for mobile phones like the Librem 5 aiming for minimal battery usage.

It supports:

  • Inhibiting suspend/idle when playing video
  • Stopping video playback on (i.e. power button toggled) blank
  • Registering as default video player in GNOME control center
  • An indicator whether hardware accleration is in use
A simple GTK4 based video player for mobile phones https://gitlab.gnome.org/guidog/livi https://gitlab.gnome.org/guidog/livi/issues Guido Günther org.sigxcpu.Livi.desktop Video Full screen portrait mode https://gitlab.gnome.org/guidog/livi/-/raw/main/screenshots/landscape-fullscreen.png?inline=false 360 keyboard pointing touch

New features and playback improvements

  • Improve hardware accelerated playback
  • Start videos at their last position
  • Allow to paste URLs

New features and visual improvements

  • Allow to select subtitle and audio streams
  • Add file open dialog
  • Add keyboard shortcuts dialog
  • Better adjust controls to wide and narrow layouts

Minor improvements

  • More visual feedback on state changes
  • Rename from µPlayer to Light Video

Minor improvements

  • Fix possible crash on shutdown
  • Allow to set playback speed
Update to GNOME 44, detect more hardware accelerators Add some keyboard shortcuts Initial release
livi-v0.1.0/data/org.sigxcpu.Livi.svg000066400000000000000000002747571457505274000175040ustar00rootroot00000000000000 Adwaita Icon Template image/svg+xml GNOME Design Team Adwaita Icon Template livi-v0.1.0/debian/000077500000000000000000000000001457505274000140745ustar00rootroot00000000000000livi-v0.1.0/debian/README.source000066400000000000000000000020761457505274000162600ustar00rootroot00000000000000This package is maintained with git-buildpackage(1). It follows DEP-14 for branch naming (e.g. using debian/sid for the current version in Debian unstable). It uses pristine-tar(1) to store enough information in git to generate bit identical tarballs when building the package without having downloaded an upstream tarball first. When working with patches it is recommended to use "gbp pq import" to import the patches, modify the source and then use "gbp pq export --commit" to commit the modifications. The changelog is generated using "gbp dch" so if you submit any changes don't bother to add changelog entries but rather provide a nice git commit message that can then end up in the changelog. It is recommended to build the package with pbuilder using: gbp buildpackage --git-pbuilder For information on how to set up a pbuilder environment see the git-pbuilder(1) manpage. In short: DIST=sid git-pbuilder create gbp clone cd gbp buildpackage --git-pbuilder -- Guido Günther , Wed, 2 Dec 2015 18:51:15 +0100 livi-v0.1.0/debian/changelog000066400000000000000000000361441457505274000157560ustar00rootroot00000000000000livi (0.1.0) experimental; urgency=medium [ Guido Günther ] * screenshots: Update to something recent. The design changed. * data: Fix appstream metainfo * tests: Disable the network access when running the. This allows to run them in isolated build environments. We switch form deprecated appstream-util to appstream-cli for that too. * ci: Run tests in build step. This is mostly validation so don't bother with an extra job * flatpak: Update gstreamer. Taken from https://github.com/flathub/org.sigxcpu.Livi/pull/6/files * ci: Move flatpak job to packaging stage. This will make it easy to identify once we have more jobs * ci: Add scripts for common jobs * po: Allow check-po to pass. This makes sure we don't regress and translators can run their tools * ci: Enable automatic po and commit message checking * window: Swap muted/unmuted audio icons. More in line with other players Closes: https://gitlab.gnome.org/guidog/livi/-/issues/13 * window: Split mime types * shortcuts: Split into general and video section * app: Allow to paste uris. Pasting an uri into the main window via ctrl-v plays that video. * sink: Fix gst debug category * build: Add run script. We have GSettings now and want to be able to run from the source tree. * data: Always compile schemas. This allows us to run from the source tree * window: Add type checks to public functions * window: Use video folder if we don't have an url * recent-videos: Track played videos and positions. This saves the played videos in GSettings so playback can be resumed. * window: Add recent video handler * window: Remember position on state state changes * window: Remember stream position on close request too * window: Seek to old stream pos * window: Add ref_uri. This is the name we want to refer to the stream. For local files it makes no difference. For https:// streams that pass through a URL processor the ref_uri is the initial short name and the uri is the actual playable backend uri (which e.g. for video sites changes with every play). * window: Use ref_uri for title. For filenames it doesn't matter and for https:// URLs the ref_uri is usually the shorter one. * application: Track the reference URL too * window: Show overlay when resuming a stream or reaching EOS. This allows to select whether to resume or start at the beginning. At end of stream we only show the Restart option. * main: Cleanup on SIGINT and SIGTERM. Makes sure we shutdown gracefully cleaning up cache files, sync stream state, etc. * window: Arm Hide controls timer when entering playing state. When the user presses e.g. "space" we want to autohide. We don't want to so on touch only devices though. * window: Switch to content view when toggling play. Otherwise we might e.g.stay on the error page. * window: Hide controls in non playback states. Error and empty state don't need them and confuse the user. * application: Move option handling into class. No need to have this split between two files Fixes: 7fdcdb8 ("Add LiviApplication") * app: Allow to skip automatic resume * window: Label SDH subtitles. We'll likely need more tag parsing here but the only example I have here uses this to annotate the subtitle stream. * controls: Make sure there's initially no lang menu. If a stream has neither audio streams nor subtitles we don't want to present an empty menu. * window: Don't forget to init autofree'd pointer. Fixes: 5c0962d ("window: Label SDH subtitles") * window: Show controls when window becomes active. This makes it more obvious the window got focused hence easing navigating without pointer / touch. * ci: Use meson setup * build: Bump required GTK dependency to 4.13.7. This allows us to buils using graphics offload Add a fallback until this GTK version makes it into distros. * recent-videos: Drop unused enum * recent-videos: Allow to get URL of recently played videos * application: Allow to resume the n-th last video * window: Remember if URI is preprocessed. When the ref-uri isn't the player uri it got preprocessed. We could determine that when needed but it's easier to just cache it when set. * recent-videos: Track if a video is preprocessed. Use that information when picking a specific video to resume. * application: Preprocess when picking a specific video to resume. If we pick an old video to resume we must preprocess it's URL again if needed. If the user forces preprocessing we honor that. If an explicit URL is passed we don't get preprocess information automatically so the user can control that. * recent-videos: Use sort_values. Otherwise we access the to be sorted values incorrectly. [ Robert Mader ] * window: List mkv files in file chooser. Additionally to mp4 and webm. The app is already registered for these file types in e.g. file browsers, but opening files from within the app isn't possible without this change. * window: Add more elements to hw-decoding check. Add stateful v4l2, va-api and vulkan elemnts, as well as av1 elements where appropriate, to make the icon and message useful on more platforms. * sink: Add support for DMABuf import and graphics offload. By implementing support for `GdkDmabufTextureBuilder` and `GstVideoInfoDmaDrm`. This allows zero-copy video playback on Wayland when paired with hardware video decoding. Mostly a straigh forward copy from `GtkGstSink`. Note that Gstreamer 1.23.1/1.24 is required. We bump the required GTK version due to the change in the UI file, which is hard to make conditional. -- Guido Günther Thu, 14 Mar 2024 21:09:52 +0100 livi (0.0.6) experimental; urgency=medium * ci: Make vars match the image. We use trixie since some time * window: Hide center overlay on play. This makes sure we hide it when the new overlay was started by a new video (e.g. via a remote instance) * css: Only make main window transparent * main: Simplify keyboard shortcuts. Avoid the need for a variable for each shortcut group * window: Remove info bar. Use the headerbar instead. This moves us closer to the designs. * window: Only show icon when accel is off. This follows the "don't irritate the user when all is fine" pattern * window: Use adw-toolbar for bottom bar. This allows us to avoid a `GtkRevealer` and makes sure top and bottom bar have consistent animations. * window: Drop margin around controls. This makes them align to the window edges getting us closer to the designs. * Add a shortcuts window. Not very useful on mobile but on desktop. * Add an about dialog. Regular apps ought to have one. * window: Add menu button. This for now allows to show the keyboard shortcuts and about window. * window: Use a vertical menu for playback speed. Moves us closer to designs * window: Reindent. No functional change. Do this in a separate commit to ease rebasing. * window: Improve control icons. Instead of specifying padding in css just make sure the button is squared via width-requests and let them expand. This ensures the whole area stays touchable. While at that use the gear icon for settings and deemphasize mute and settings a bit by using a smaller icon. * window: Add a single method to play an URL * window: Add initial file open dialog. We want to improve the mime types/file patterns and sync it up automatically with what we have in the desktop file but let's give users a UI way to open files. This helps cases where livi isn't the default video player and launched via e.g. nautilus. * window: Strip blanks to save space in resources * build: gitignore packaging files * main: Use AdwApplication. No need to init adwaita separately then. * Add LiviApplication. This allows us to carry application state * application: Move startup handler. We can handle them via a vfunc in LiviApplication. * application: Move activate handler. We can handle them via a vfunc in LiviApplication. * application: Move command-line handler. We can handle them via a vfunc in LiviApplication. This allows us to drop the context and properly dispose the url-processor object. * application: Avoid g_object_{get,set} We have a proper object to keep the state now * window: Move empty state closer to designs. Add a Open… button and use the camera icon * window: Move error state closer to designs. Add a "Try Again" button and use the warning icon * window: Reindent. No functional change. Do this in a separate commit to ease rebasing. * window: Use a parameter on the action for seeking. We use millisecons resolution as we're limited to int32 ('i') as int64 ('x') doesnt work. * window: Split out controls. If we want to make the controls look differently depending on width it's nicer to not clutter the window class even further. We move the controls over 1:1 (not even reindenting ui) and wire these up with a minimal amount of setters. * controls: Reindent. No functional change. Do this in a separate commit to ease rebasing. * controls: Add Stack. What we have at the moment matches the wide layot * Add skip forward/backward icons. Taken from phosh 0.34.0 * controls: Add initial narrow layout * controls: Add narrow property. If set we use the narrow layout. We need to switch over the popover as it can only have one parent * window: Toggle narrow and wide layout * controls: Clamp and float the wide layout * controls: Add skip forward/backwards buttons to wide controls * window: Make sure we don't skip < 0 * window: Avoid icon if we seek to the same position * resources: Sort icons alphabetically * icons: Add region-symbolic. Taken from GNOME Settings 45 * window: Move stream related bits to struct. This allows us to reset in one place * Handle multiple audio stream languages. We ignore different bitrates, etc for now. * window: Add subtitle streams * controls: Add style classes to playback speed menu. This makes the title style consistent with the new lang menu * controls: Reindent. No functional change. Do this in a separate commit to ease rebasing. * window: Avoid multiple seeks. If a seek is ongoing and we didn't set a new position yet, reject it. This avoids the overlay fading out too quickly. * Add utils. There' some macros we want to share between modules * build: Specify gstreamer version in one place. Bump to a halfway recent version while at that. * window: Use win.ff action for forward/backward skipping. We only kept win.rev for the keyboard shortcuts but can also handle this by giving the action detail. * window: Make player state a property * window: Track stream title. Without a title try to make some sense of the URI. * Export mpris interface. This exports the interfaces and wires up raise. No actual functionality. Phosh doesn't pick it up yet as we don't set a proper player status yet. * ci: Remove unused remote * window: Enable visualization for audio only streams. We just use waveform for now and fall back to the next best one if that's missing. * window: Handle subtitle selection * window: Autohide the panel. Show on pointer motion, keep open when pointer is over ui elements Follow-up commits refine this. * window: Hide cursor on inactivity * window: Only change icon on fullscreen. Let autohide handle the controls / bars * window: Unfullscreen when file choser opens. Otherwise the portal's dialog can't set this window as proper parent. * window: Only hide controls during play * window: Only hide controls when there's pointer. Things are hard to use on touch otherwise. We still auto-hide controls on state mode changes to play. * application: Use GTK's window.close action. No need to have our own. * main: Add short-option for --yt-dlp. I'm using it often and keep mistyping it * window: Use stateful actions for subtitles and audio tracks. This makes them automatically render correctly as radio buttons in the popover. * controls: Hide the wide layout in narrow mode. Otherwise widgets in the wide layout can prevent the window from shrinking further. * controls: Use a bit less horizontal space. Othrwise we overflow when the lang menu is open * controls: Use win.ff everywhere. Fixes: 19aeb15 ("controls: Use a bit less horizontal space") -- Guido Günther Fri, 19 Jan 2024 17:09:54 +0100 livi (0.0.5) experimental; urgency=medium [ Guido Günther ] * Update copyright. The files this is based on are LGPL 2.1 in GTK so keep it like that. * ci: Use Debian trixie * build: Require gtk 4.12 * sink: Avoid deprecated gdk_gl_texture_new. See GTK commit fa44d258d090cc44bb3fc6f21b4d9f32212ae2b8 * paintable: Avoid -Wfloat-equal compiler warning. Use G_APPROX_VALUE * paintable: Move interface definition to the top. As we do it in phosh, etc * paintable: Use automatic cleanup for context * sink: Use automatic cleanup for the buffer pool * sink: Use automatic cleanup for texture * sink: Use automatic cleanup for tmp caps * window: Add visual feedback when skipping ff/rev/play/pause. Show a centered icon indicating what's going on. * window: Drop unused variables * window: Indiate end of stream * window: Track warnings. Let's make sure we at least print them to the console * window: Show overlay icon on slider value changes * main: Don't hardcode window height in two locations. We have it in the ui file too * window: Use a larger default height. Otherwise we cut off parts of the page * window: Drop some unused variables * url-processor: New class to fetch URLs via yt-dlp * main: Add --yt-dlp command line option. Closes: https://gitlab.gnome.org/guidog/livi/-/issues/6 * window: Bubble up url-processor error messages * window: Use livi_window_set_error internally too * main: Allow F11 as fullscreen shortcut * metainfo: Remove .desktop from app-id. As per https://freedesktop.org/software/appstream it's not part of the app-id. * window: Use same margin for info overlay than for controls * window: Use AdwApplicationWindow * build: Bump Adwaita dependency to 1.4. We want to use AdwHeaderBar * window: Make top bar transparent. Gets us closer to the designs [ Krassy Boykinov ] * Rename µPlayer into Light Video Closes: #4 * Fix format string for 32 bit architectures -- Guido Günther Wed, 20 Dec 2023 20:39:20 +0100 livi (0.0.4) experimental; urgency=medium * metainfo: Update URLs and formfactor * sink: Use automatic cleanup * main: Don't forget to destroy main window. Otherwise we won't clean up the GST elements either Closes: https://gitlab.gnome.org/guidog/livi/-/issues/2 * Allow to set playback speed -- Guido Günther Fri, 28 Apr 2023 18:34:25 +0200 livi (0.0.3) experimental; urgency=medium * Initial release -- Guido Günther Fri, 31 Mar 2023 12:09:19 +0200 livi-v0.1.0/debian/clean000066400000000000000000000001161457505274000150770ustar00rootroot00000000000000debian/phosh.service debian/sm.puri.Phosh.service debian/sm.puri.Phosh.target livi-v0.1.0/debian/control000066400000000000000000000016301457505274000154770ustar00rootroot00000000000000Source: livi Section: x11 Priority: optional Maintainer: Guido Günther Build-Depends: appstream, debhelper-compat (= 13), desktop-file-utils, libadwaita-1-dev (>= 1.4), libgstreamer1.0-dev (>= 1.22.0), libgstreamer-plugins-bad1.0-dev (>= 1.22.0), libgtk-4-dev (>= 4.12), meson, Standards-Version: 4.6.2 Homepage: https://gitlab.gnome.org/guidog/livi/ Rules-Requires-Root: no Package: livi Architecture: any Depends: ${misc:Depends}, ${shlibs:Depends}, Description: Minimalistic video player targeting mobile devices Livi is a minimalistic GTK4 and GStreamer based video player for mobile phones aiming for minimal battery usage. . It supports: . * Inhibiting suspend/idle when playing video * Stopping video playback on (i.e. power button toggled) screen blank * Registering as default video player in GNOME control center * An indicator whether hardware accleration is in use livi-v0.1.0/debian/copyright000066400000000000000000000033061457505274000160310ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: livi Source: https://gitlab.gnome.org/guidog/livi Files: * Copyright: 2022,2023 Guido Günther Copyright 2021 Purism SPC License: GPL-3+ This package 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 package 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 . On Debian systems, the complete text of the GNU General Public License version 3 can be found in "/usr/share/common-licenses/GPL-3". Files: data/icons/view-restore-symbolic.svg data/icons/speedometer4-symbolic.svg data/icons/speedometer2-symbolic.svg data/icons/play-large-symbolic.svg Copyright: 2022 GNOME Design Team License: CC0-1.0 To the extent possible under law, the author(s) have dedicated all copyright and related and neighboring rights to this software to the public domain worldwide. This software is distributed without any warranty. . You should have received a copy of the CC0 Public Domain Dedication along with this software. If not, see . . On Debian systems, the complete text of the CC0 1.0 Universal license can be found in "/usr/share/common-licenses/CC0-1.0". livi-v0.1.0/debian/gbp.conf000066400000000000000000000003321457505274000155110ustar00rootroot00000000000000[DEFAULT] debian-branch = main debian-tag = v%(version)s debian-tag-msg = %(pkg)s v%(version)s [tag] sign-tags = true [dch] postedit = sed -i s"@^\( \+version: '\)[0-9.]\+\(',\)@\1$GBP_DEBIAN_VERSION\2@" meson.build livi-v0.1.0/debian/rules000077500000000000000000000003021457505274000151470ustar00rootroot00000000000000#!/usr/bin/make -f export DEB_BUILD_MAINT_OPTIONS = hardening=+all %: dh $@ --builddirectory=_build override_dh_auto_configure: dh_auto_configure -- $(CONFIGURE_OPTS) override_dh_install: livi-v0.1.0/debian/source/000077500000000000000000000000001457505274000153745ustar00rootroot00000000000000livi-v0.1.0/debian/source/format000066400000000000000000000000151457505274000166030ustar00rootroot000000000000003.0 (native) livi-v0.1.0/debian/tests/000077500000000000000000000000001457505274000152365ustar00rootroot00000000000000livi-v0.1.0/debian/tests/control000066400000000000000000000001121457505274000166330ustar00rootroot00000000000000Test-Command: /usr/bin/livi --help Restrictions: superficial Depends: @, livi-v0.1.0/meson.build000066400000000000000000000046531457505274000150240ustar00rootroot00000000000000project('livi', 'c', version: '0.1.0', meson_version: '>= 0.63.0', default_options: [ 'warning_level=2', 'c_std=gnu11', ], ) i18n = import('i18n') cc = meson.get_compiler('c') display_name = 'Light Video' config_h = configuration_data() config_h.set_quoted('APP_ID', 'org.sigxcpu.Livi') config_h.set_quoted('DISPLAY_NAME', display_name) config_h.set_quoted('PROJECT_NAME', meson.project_name()) config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) config_h.set_quoted('GETTEXT_PACKAGE', 'livi') config_h.set_quoted('LOCALEDIR', join_paths(get_option('prefix'), get_option('localedir'))) global_c_args = [ '-I' + meson.project_build_root() ] test_c_args = [ '-Wcast-align', '-Wdate-time', '-Wdeclaration-after-statement', ['-Werror=format-security', '-Werror=format=2'], '-Wendif-labels', '-Werror=incompatible-pointer-types', '-Werror=missing-declarations', '-Werror=overflow', '-Werror=return-type', '-Werror=shift-count-overflow', '-Werror=shift-overflow=2', '-Werror=implicit-fallthrough=3', '-Wfloat-equal', '-Wformat-nonliteral', '-Wformat-security', '-Winit-self', '-Wmaybe-uninitialized', '-Wmissing-field-initializers', '-Wmissing-include-dirs', '-Wmissing-noreturn', '-Wnested-externs', '-Wno-missing-field-initializers', '-Wno-sign-compare', '-Wno-strict-aliasing', '-Wno-unused-parameter', '-Wold-style-definition', '-Wpointer-arith', '-Wredundant-decls', '-Wshadow', '-Wstrict-prototypes', '-Wswitch-default', '-Wswitch-enum', '-Wtype-limits', '-Wundef', '-Wunused-function', ] if get_option('buildtype') != 'plain' test_c_args += '-fstack-protector-strong' endif foreach arg: test_c_args if cc.has_multi_arguments(arg) global_c_args += arg endif endforeach add_project_arguments( global_c_args, language: 'c' ) gnome = import('gnome') subdir('data') subdir('src') subdir('po') run_data = configuration_data() run_data.set('ABS_BUILDDIR', meson.current_build_dir()) run_data.set('ABS_SRCDIR', meson.current_source_dir()) configure_file( input: 'run.in', output: 'run', configuration: run_data) configure_file( output: 'livi-config.h', configuration: config_h, ) summary({ 'Gst dmabuf passthrough support': dmabuf_passthrough, }, bool_yn: true, section: 'Build', ) gnome.post_install( glib_compile_schemas: true, gtk_update_icon_cache: true, update_desktop_database: true, ) livi-v0.1.0/meson_options.txt000066400000000000000000000002011457505274000163000ustar00rootroot00000000000000option('dmabuf-passthrough', type: 'feature', value: 'auto', description: 'Whether to enable dmabuf passhthrough') livi-v0.1.0/org.sigxcpu.Livi.json000066400000000000000000000057151457505274000167270ustar00rootroot00000000000000{ "app-id" : "org.sigxcpu.Livi.Devel", "runtime" : "org.gnome.Platform", "runtime-version" : "master", "sdk" : "org.gnome.Sdk", "command" : "livi", "finish-args" : [ "--device=all", "--share=ipc", "--share=network", "--socket=pulseaudio", "--socket=wayland", "--filesystem=xdg-videos" ], "cleanup" : [ "/include", "/lib/pkgconfig", "/man", "/share/doc", "/share/gtk-doc", "/share/man", "/share/pkgconfig", "*.la", "*.a" ], "build-options": { "env": { "GST_PLUGIN_SYSTEM_PATH": "/app/lib/gstreamer-1.0/" } }, "modules" : [ { "name": "x264", "config-opts": [ "--enable-shared", "--enable-static", "--enable-pic", "--disable-lavf" ], "sources": [ { "type": "archive", "url": "https://download.videolan.org/pub/x264/snapshots/x264-snapshot-20191217-2245.tar.bz2", "sha256": "0bb67d095513391e637b3b47e8efc3ba4603c3844f1b4c2690f4d36da7763055" } ] }, "build-aux/flathub/shared-modules/gudev/gudev.json", { "name": "gstreamer", "buildsystem": "meson", "config-opts": [ "--buildtype=release", "--wrap-mode=nodownload", "--libdir=lib", "-Dbase=enabled", "-Dgood=enabled", "-Dbad=enabled", "-Dugly=disabled", "-Dgst-examples=disabled", "-Dqt5=disabled", "-Dtests=disabled", "-Dexamples=disabled", "-Dintrospection=disabled", "-Ddoc=disabled", "-Dgtk_doc=disabled", "-Dgst-plugins-base:orc=enabled", "-Dgst-plugins-bad:aom=disabled", "-Dgst-plugins-bad:v4l2codecs=enabled" ], "sources": [ { "type": "git", "url": "https://gitlab.freedesktop.org/gstreamer/gstreamer.git", "tag": "1.22.8", "commit": "4af14db10e8355f980bbf79733af004e59d255fc", "disable-submodules": true } ], "cleanup": [ "/include", "/lib/*.la", "/lib/gstreamer-1.0/*.la", "/lib/gstreamer-1.0/include", "/lib/pkgconfig", "/share/gtk-doc" ] }, { "name" : "livi", "builddir" : true, "buildsystem" : "meson", "sources" : [ { "type" : "dir", "path" : "." } ] } ] } livi-v0.1.0/po/000077500000000000000000000000001457505274000132705ustar00rootroot00000000000000livi-v0.1.0/po/LINGUAS000066400000000000000000000000001457505274000143030ustar00rootroot00000000000000livi-v0.1.0/po/POTFILES000066400000000000000000000002321457505274000144350ustar00rootroot00000000000000data/org.sigxcpu.Livi.desktop.in data/org.sigxcpu.Livi.metainfo.xml.in data/org.sigxcpu.Livi.gschema.xml src/livi-window.ui src/main.c src/livi-window.c livi-v0.1.0/po/de.po000066400000000000000000000100151457505274000142150ustar00rootroot00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the livi package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: livi\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2024-01-20 14:19+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" #: data/org.sigxcpu.Livi.desktop.in:3 data/org.sigxcpu.Livi.metainfo.xml.in:4 msgid "Light Video" msgstr "" #: data/org.sigxcpu.Livi.desktop.in:4 msgid "Video;Movie;" msgstr "" #: data/org.sigxcpu.Livi.desktop.in:5 msgid "Play videos" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:8 msgid "" "A minimalistic GTK4 and gstreamer based video player for mobile phones like " "the Librem 5 aiming for minimal battery usage." msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:11 msgid "It supports:" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:15 msgid "Inhibiting suspend/idle when playing video" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:16 msgid "Stopping video playback on (i.e. power button toggled) blank" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:17 msgid "Registering as default video player in GNOME control center" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:18 msgid "An indicator whether hardware accleration is in use" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:21 msgid "A simple GTK4 based video player for mobile phones" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:26 msgid "Guido Günther" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:42 msgid "Full screen portrait mode" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:61 msgid "New features and visual improvements" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:63 msgid "Allow to select subtitle and audio streams" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:64 msgid "Add file open dialog" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:65 msgid "Add keyboard shortcuts dialog" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:66 msgid "Better adjust controls to wide and narrow layouts" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:73 #: data/org.sigxcpu.Livi.metainfo.xml.in:83 msgid "Minor improvements" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:75 msgid "More visual feedback on state changes" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:76 msgid "Rename from µPlayer to Light Video" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:85 msgid "Fix possible crash on shutdown" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:86 msgid "Allow to set playback speed" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:92 msgid "Update to GNOME 44, detect more hardware accelerators" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:96 msgid "Add some keyboard shortcuts" msgstr "" #: data/org.sigxcpu.Livi.metainfo.xml.in:100 msgid "Initial release" msgstr "" #: src/livi-window.ui:37 msgid "Fullscreen" msgstr "" #: src/livi-window.ui:153 msgid "Open a Video" msgstr "" #: src/livi-window.ui:156 msgid "Open…" msgstr "" #: src/livi-window.ui:171 msgid "Unable to play video" msgstr "" #: src/livi-window.ui:174 msgid "Try Again" msgstr "" #: src/livi-window.ui:224 msgid "Open file…" msgstr "" #: src/livi-window.ui:230 msgid "Keyboard Shortcuts" msgstr "" #: src/livi-window.ui:234 msgid "About Livi" msgstr "" #: src/livi-window.ui:241 msgid "Videos" msgstr "" #: src/livi-window.c:411 msgid "Choose a video to play" msgstr "" #: src/livi-window.c:460 #, c-format msgid "%.2lds" msgstr "" #: src/livi-window.c:518 #, c-format msgid "Buffering %d/100" msgstr "" #. Translators: None here means: disable subtitles #: src/livi-window.c:705 msgid "None" msgstr "" #: src/livi-window.c:729 msgid "Languages" msgstr "" #: src/livi-window.c:731 msgid "Subtitles" msgstr "" #: src/livi-window.c:826 msgid "Video ended" msgstr "" livi-v0.1.0/po/meson.build000066400000000000000000000000451457505274000154310ustar00rootroot00000000000000i18n.gettext('livi', preset: 'glib') livi-v0.1.0/run.in000077500000000000000000000006501457505274000140120ustar00rootroot00000000000000#!/bin/sh set -e ABS_BUILDDIR='@ABS_BUILDDIR@' ABS_SRCDIR='@ABS_SRCDIR@' GSETTINGS_SCHEMA_DIR="${ABS_BUILDDIR}/data:${GSETTINGS_SCHEMA_DIR}" export GSETTINGS_SCHEMA_DIR [ -n "${G_MESSAGES_DEBUG}" ] || G_MESSAGES_DEBUG=all export G_MESSAGES_DEBUG if [ -z "${GSETTINGS_BACKEND}" ]; then # Make sure we don't mess with the systems gsettings: export GSETTINGS_BACKEND=memory fi set -x exec "${ABS_BUILDDIR}/src/livi" $@ livi-v0.1.0/screenshots/000077500000000000000000000000001457505274000152125ustar00rootroot00000000000000livi-v0.1.0/screenshots/landscape-fullscreen.png000066400000000000000000006142111457505274000220170ustar00rootroot00000000000000PNG  IHDRhD[N IDATx̽Iɕ.v<,&RJj_36 < ðwW^{gxW~`nZURJ5W8^^rs;EC ͌8 T??# "!.GD2g L{x枈x==3M!XB]mל1#Fwlmq3DGuˊm}0˃CQh8`|tɳuIcN)Cĺ @HdI }/mR__'xZ*r."%jX86dd\[W_kW^[n_wg!s:g~UERU(f&cw=JՕ4K fu HJ9< :TZ/u Ąw$q!"! 1hRΊ^et2Ngy)G!<ϙeYfE1+H̎̈Q1+j}̐ZBK4M$I@%CCޛY=՚zVb1l:^˿`%;~؅G`I DD<._I+{Z-X:d]^K/x!^x&B\(rG`W*P++1FBDP4ST`ST$ ԐPTFDTH JLMA@MA"j<(0GgjcTf&(]瘙̎'~@J/RjltJ{(gV3j@Ŧhfn50!h (1/!!jXױtZGwOn޼YVE*Nf~b}YGnVLb ޽ zHG7ͷOnO m2UnݽOIW^{'?##Dd"C:Ndι$1#3!8@:4fA(EH ʠ" UQ˙HI݈I[5Np /yiQMgEYMn߾" Vv׷iD_ՔX}Q 0"|WRрU4ā1(aXlf!LlCZK")3gLŒIAD@D09 MR"Q̠y5")s׎]$ @DNY2G$˚׏@D0Pm YQo}?Tov#ЎU-3>Ŧq_Mr9xo:=5gw:^hoogї7kv-׶ѧy_.V~,|zϴÏ$Q@$QBɫ?J`FHTL*(*5 E1#V5DPQA"j je(**"iMѐ$JM{^u>%j4QhXI%rPVpk;lA!Il I/8wݳ%<l0fV("&fzc':@30[ 0 QB*ԵZ*(QL"h1ZE5GG}tƲBu4s'wD_GgN(si\^*`Ʀ}L#g!<NBmAp{ $Y;s.]ւ3fN\J1O94QE T@L M"114KXYE QTP )*r32dfdfj $ `An-qd &bf!|>xBQDtn, MCa<(ð;k=,J X 1TOe ,_ 6S"AdG319!Op_ɶPԭԠAzkUF_K]B4u:L&_><:$K$j Q(E:@3$‰on[|{lmW}@n\޻fVOu굗_7{$DJ\H4ʢfÝp$NCLngmTE%%wҼA.羌U-eʢ#t++/fW_ 㓃{w?|XfYeRtzjw{ê $%D/2"/10Dˣu<7@0Bdl!AuR.b4$~whf0"Dbbn.+! TS M@93Q3.̌IY)!0AUE4ĈH hZU$GDO p1ޢ(=o<YelB1V’|T_OS3 sc1͝TXZZDwyڂK `z/N |_U6`Ũ)ԀEw)ːK,uY褚Wh Ŏ&*D`*,""THH XMEd᧡K" 3잚"^*STc*QA4(EE4!Ή(LP| 1hp E*b=62X_`'}tZc)YLAC@&LRրFV4(hbJ"Ve^c{K|˲M7o޹}okv=WT!u]OA!Tiw+vk'zɣEȲt^Xŀ Ͽn8{w߽e{`:Jz4 ,Y1v %y7Vu`FvfXWuUU%uDEԈ|P.Mb9f@DZ P_:L9i(B@04MTb|(fjg۳VOW 3b S {lr=iO->EϷH%_ӳ^y?V^e\ 43s7X̠e˼֣p^.>a[ , ;c д1;h*f `-C*ɘ0"" *i6e(s 3|@Cf b j' 6t@XЉY[Yᴂqign9cO:_m/ [/B$0MSaWiL@-KUǪV%+cRa<N=4{ݞ5򸨽Yu<:| Z.k!qwk{s}ɸ ?|kY١!4{ZQ뮬|wz|O?x:͝P8<9)tӬ}^X]'GG #: ӮV!ԾBYȄ.9qWv_v7Aр@hM`v:g^oߖj|{i%2ԙqY^;GUq.%PH\Jʒ&`&>XA O'e11K$TB4P&0#sb *Jľ{SC3 1\ A8XUBT3T@U UwYDJkf"HJЄ AW "?Xy<ϲ,MS3/u]WUպ<ㆆ(2o0kG+<6{=́O=0wYxRp<U~~I7H]Vk;hYO|(**HD`sѪ: !1'iw:ٴY${Bh`=*q2\KWW۬(E;W |>!aCP!60@8Ͽx#=x In/*=*' M`\"88gn'e^#S(*WV[u]B_{eōK|:bG)c@Ss l-pŬ'EG`p$T#BAT 4!J #ͩPh<F™p9j.YljT:41ç-7 7tI IQ34v''a:X!#&~DIgkT# R-8׈ffMH 9U;"!)G(&6t6hY,6#Mb4%@Y'ܓp$ YYn @tO^xT/UU$$BBpl s[@EPE1)յu,j̦uYVeg(xx<W^|jg},s0l 0*˲8:8*utDفmEUr"gq>!1[O?;{|ׯB"f@Gh3pݪ]7X*)b]Z]{p׫0y~b`̼&e=M&2&^n.+(F:~dڍ7瑝`Nq4_h`ANRNY >Q$U5$tPPQ%5E4M;;/w[7n9|O9Lf'3RvJP f#B 1PL,N$a"ޛ  b,:2hi S8g&N3rQ 9s[Z bl ԉQ F eHB@xP4+pt6yy6/В7-뾤NO?*<%hMl>SQ\7Tڶ1Smg۹53YK/ 'yE= WYݣxG2pTh=Cp<ӏǥ̡!PⲼSXtU D*BFĄ1*Q*/ ":%Z *0,?.6t78 [ₚX 3$QH: ^KQEW.3 3*_^Yn0ha _z2QK0/! CbU/X1t:-Yu|<pAWzkvʕlg:]&riQŰ6o~6'Irղjս[qU<9:q_} @w~/|?x\F24C@34tv?{W"6WҎ3TP)'pZz z/}w~O (ڕ9L:r$see 4P($>HfiMQ'IX$us_4ht~?>F#"ITtoĤUsŮA2cr(&:s婄#EL )`pRvL.$焉sQk(6 |s$_u=Ԁ뭭9թf^[[>sg-<<6dp}:q~b[7]Yfl<6ٖKe d@l Vҙw$B$2;J CPXDQ qBQI XbTxhmN SEs4R*2jDI"bs76XyPblTo!!2[tFZEIy楲@ y[k0h-W9㍌Ho@` S0H=9s)abJuKo*̪|,8MLJÃwE^H;=qIr׮``VŃ J&4q0#BCߋQj]b ~Qӯo~UO?o~v0M~nGHN##Kf)!~rާo-kJg˪I9A~.td@!ͦĢƘdg}s`bV`GsIE_~sJhF3CރӷѺLRP*~%[n~0` M Uv.4Ѻb`,,"r\}iue`<&pQ0w,k*듣 (*A"zV1+giƄԘ"D0H$v!A" Th aCB&iΉUQK'Ā(3&2qq.il Qkkkgw_/ԏ׿s5Hڅ^]8̗%Nm*^! `u7d0w=]9BNz$CQK#x#Gٔ%ҹ9f IDAT4]Ͻ 0G֨RKxv؛KnE=55 4LY 4hC ˺2j<4}ٛƧ{雱(N_Cls'q3hLsF/ZjxxڂxF=7eM-x+1ӹvF>2 _].z+ S?dzKo/3-3s}=>Ъwg$I2mlX1F;w'HM& Fcʘ/o0@jL ̀[;z뼱Z{7mA DЦDCSjBZ()!ň^ɓ>|$f/aEDv#Dׄ1#1XɨliQA&b;z/1ՆZ6,Di@ 9V2Ҿɵ.Ap{-*Z{XW2;{14ʬCYYKՐw$/g$q}i4=XY]9y8 hS28DTHZ"?Ѡ?p޹W\|8i_{}2 32-~wN쭭ׯ^<җ!rZdt_{s"1#\_^C$Dq ŦB>͍͝{esĻY~ޯo}~n%Lzt^٬>.ͿDb ĜL+;oUyuY_|Xb,%+ݎ K2jrVk pT6:n\Qß317  NSCwxj'{ .\tV}Z'70҄4Ek 0_|}W T5Jdnj(?EI,Apn^eBx{<Hdgg71XQƲUk/ދ*hwށ#9Nom>wy̐9K:9W't"g:aNԌ  SUUeU $(F JswG)''踜M|1={yǣ[_we$?Oٱ/(Tjl,$K8O Wͯ]!~ZXPp4;\hTT~s{77BL8x,ů~P*'>Vsw rst(I@C^ՃL9 o[{i>x$۹v_w+j@hX͊O`::IL,K%r%KmYBe͉EngМcSlq' b0r!…xZ=ѻ\W<{%/= O2k?Rb"* dDí;ӀZl2N49[9-vL#Xcb_=jj7ٰ`LK$q.!gq,*pc"t9pz=pV_6]pyh e쐭zc7Ёyƚ[1`$ZJ*V^*qw``a0bHs=rJRlW$)NOFGãj6EA&f HY's몮khrq!&OW:OA][]۽K_}^1=)&[};8޽/>x:TProޝ;_<ؿkV9ht2L,!GMZZ3$vu8L'jRTE 1KbYi^bCTG|fL qw~oHT@ھϦ?񻿬ԾB!_{NY.bѫb<ƌn"bADE,׍ * _/ѻh=r,~4ɢ$@夘fgdzIqbz D$1VE|P v_{2;eQò(&B&x2wi]}I]45t:4`Md!_'??s1uhu\i?TMX Mg??{/2[[Wѿuz]CRlIm$.zӏ겆fG}18DMt7!b5NgeY^;TbA,`&gլ9/承?\BejJd|uc?CL\#p郱̼j`|z1yyWz^}TYQfKO%C9-Ηwq>q1$5q<<۵gϞ,k'w#m_5?.v46~8$DG,Ih6Jt@ѡH$&} ȜAUlwUmkfچ0!* )"2 "@QA ^@H jJFh}]Ep.,\6ܸU6'v=,9P"K9e}Nz;I1RױB Ť:>fBEC!wpE\&Mtԣ[}Ԑ.K&I+b⣯c@I6U};5kD ~Ȝ Ѵ,IL aVY@uY g1XMgU33)491U7v7W6_8 Ddv4ߗi]_)Xb2Ĭe5"cǘ>TutyUQMH Uu|)IBĪѺy2ⷿ?W?[oNm2m9F&SA1Ɛf)sNjοun5Luor&^A8cGucHYQYg!dNzQB8 LMإ0few<'ŌȐ[̻$3Ge{{{PvvvIIeQfw>Wk'ႅ |)\\Hqa#KshSʙ.kI. Ԧ+nBss#{9nO|XmY3I,}}5F`EA[ت6|eSln7!"df"y,aBlh,`߁8BlU.vGgN4IRvLĈ((!~65)==}TS5.t1mnBB`n0bζŗ$)8 6 cԪŬ%D^|iu|<ڻfϚ%W2 gA#p_;k I뀀W=~,?b3biբՃzj%|ԹK~O|Թ }^SOjX~dU=mc5kodGjQ,ϜĄBmLxgk3ݗ- @x`Ut5ܼrfE, Ah<޸y}syA믽|Wj[bUi=cFDgX94CCC]lk?!"YFy!i-bZY])ʢ 19o7ٳDJrrg72RXʝKy_K?a{LHZ8yNd{$s+GwȻy?y'z7˹ҬK=d'*Gߔ;{ZM5ݺm = ;BAbjbIz )"9̤y ( D&"|obt1Ĉv)+LU0Nzlf2x(gޱK Fddrsg|~~|zՈpZ}Dq`tЌ͓-vϊ?EeGhRWjڈQ&ֳv2mlb0Q!ezzpvtTcf7ƵB=B\u]jqaY3kv}';&HH`Fr)91ƶ-:GzÐ}ovxk|Z;M+vyzq K[_o& DZ@E"tT9T2tUӶA Ycn^yed#;;7;s ,/IҏG6x:ӹ~LFE'>Z315շ_]y[B`Dm4#& !`B%ichx:˯vދgwlbE]51tw=Һ hDF<8FBPW^|5I5big`a8,GEQFӍHڶ_yJes0­w vwtG}fH~?ʼ+J͛7Ch~2Օ>AiDv` /<_|qw}[ DŽFRiGiC im&MۨJ}tisUܩ5#8 -{e89iߙyA~+6lomQ}Hᰪlv`0x˷+W|_~z3?3eٗj^ IDAT;c0޺?/ӕm1ÏнCZޜs6n!Ԏx4PD죇CnSޥk yuq8T{>gkޕ#߷|1mx;"ҧ|(5eJD攙Rit:@03ګT:a%4FH+Ð E9!Eۖh {⺌)ZĪdy^Y,$w̐,-.,uRˈs[8w9ظQbm!C1va A@4jU;϶7g&޲>ۿ|]޺%&ѥXo\}[?Ǜ1sӘ!j0]H˜wHH4m ̒Q;y0QTb$szWs9s!5&DsĄh"<PS7Qb.+rocȐN"CP4ѬZtRtw* `N]_;ɵk/=+pӍ(mr&xiD"3Td455$@MQThD&C$(RY31A%<Ѝ9Govx$4ve(˲(,<ܵ@t;}Ƞ 0a.I-V {pBsv#*38\ZЁ =bٔmn&"ZmPM͝s༲ yo型幻%ˆ ynݾQU#DUScfQ峗/ W^P{IYg (DU$"1Ʀ 1J'ПثDd~<[P3V1R$ ¡cC*XR7,Ԕ179@ 6b&6e^HDҫB++9ZM*3E+ MMӪ(B4Bk׶6+O~"/K6߾wgQtqeq&Ub ,ϙXb HHM-9Rc@6ui Jm7- ATMNG}2 ",j\Wu3@iPm۶ }d:9^YZ򬮦MSm]'hY$9X¯ʹs`;s@W|[BL=4w~OAڢ=e7IGD})ܼXgcGrxLIrHyǿeYB^zOy0񃥓_+=9/3j09M[?z1>}SQIʭf[ؓcTu΂JJD\ + ULTEVm% U%"(yd:Ve0RU)"&qpQU4P:RDw5cR#4 "HbDQN'eYò(ۦIb @(8(~wێ춹>}ǁPa#A?3D va,X5nBňI'b%λK~e5:t盈(!4ME թ(̎)uRv Ј9*^fbDYU4F ڔ9˅، =zVM&d&!ft`$#GQ0Ѩrpf>8hآYT=c MS^o}~鳟 O<ķpt`"`ѕYcpxRA!'G\)X&oO}Ё7^{wOS?ǚWJ0::(Htfr q7#OBit&; ;43@S9NǓm,sݑLș)4S NR)^ΔpcZwМYM$8WRKn B:WH  ňmhY5z(ع!$pmv:^Y[{fA'uз.6ZSf:ƈ1C] K9f]XD64mhBQT1itZ!:z /]yO:W9‡~HLE(jkl^}cgkcgcnjNAOyA6Σ $Hl&b"o @0(@Ģȥn;29cfBk}EQ6-kÅ4-zE `EA9-.-U SKBfEBDC@ODn/W.,[VF" XP1Q&1%Lcز: 25hhq ϯVڶk7v6ƣq5]Qˋ.\(^lZIFgΞO|ӧ^z뻲r.b릭Z]ԶB[ס,F-zՕ<ٛkm, !s:8R]s,|* #""bh޹ΝC+y{oU =p|/^6` 'xw!}#啮1*=?~9^Ffwۭe{R;RCv>*hu-&="SviW~[hpLVs߹]N6`ǎ;#4w5DDΓ !\pREZyfJl"XG0͆^lB PPb0ST"sjil:Xgn[SQL@%hLޛj4%A=Qs鵷~?l4{g޸̓LLf` *(*i@%GLydЄg TAj owv7wnl͹ .wJo8ʂi?>'>:e1y_XL_yN9x6FUT{"/Dfb[M$ f>X# .^K_^"hý [~vE$4q;G@#,00~smkw>`o@ ^я8R>Lo#K1<Ox'̚S?rFmc(5w(@{rmΒɞ ;0+%b;l;W4`>sDFjf f NIL:\jdٌ11,HFJj)pX՛13krQ!MTN1'Qozy^8IE ¬mU#bLYҒ^&/nۘɌw{N= `&KN (K2_&Ã%IƘdZm4ΨULL쌳م ߇y4܁:2o~NzԍG@sSxc+{aN 6!JBDߜgBc޻(2Yƒ "yVM31S+uf!SPS35R&bf/b*bar ŕ庮7vwƱ-W1H@VNLGvo=}hͅ| U~q'~_z_L8H"vgWI\%`GFAbMo7l98`K˒B S ̃aK/O旾%3?{W_=ɹpxx?kkp*@P+bL^u.5e||^ G[*sQ?n[Jw3z F d ;WNX>4! }iq˧^3֍W}vںJEHHd`Z=LX!-PR3,婗L`ePŒ21"z R!n92AQ@# "yɱQfd2X +v-w4T.۝H_Α Y''jhL O-RɱÌM`1jNGuUޔ{|^^Xj0٘54CQ{HmvGM!;_83z@UH 0NΑct¡0/r18$3@QzQcB[,ĐiD ZEŚhέY/'ݝJږ<_~OJۊ %RrUE`5S Fx{2;}p'?hB;ھnniB#5yPגj$B{Ӌk+gofZE*Ɉ Ib3|-סOBD5zffgc}CO ضm[WU5!6{7  "#E z{aQ=Յc=hj`DŽab`4=.>6$.n`Qb31cVUQzR?__7<7G|W򕓜 a. OxGZ@xyfq"*7af^#K"z`yooQo3x=(9  <8y~5}Za="|f\6ݭӸok2$_ڲ1Lw]rMh%"&jCM͐;`l)T(YgL$"8A1"v6Mƣq˼F473bd/&6ɘJn'FW~;1UݱHiBdd].y1.hB:w2C CJLSZiX͚ٴVb'DUZ\I`.ϊ{,~2ɬRd"o TYͶGׯFy23~S$֕J {|F1eFcl%d(Fжm[QcL\Z7;h&8Gx !Bitꙕt2ݚjAV(pךjwyefm%dU }q`~gG;(aϼj&jf"'$4BMZK>ٟ ? 7us}T0AT Hd" ?{%Kѣ[7{G, / "MHB(DAM6wI5+["gB_zEp\tֶ?g?T0f:8a.Sՠ%X&CW5azꗿ˗/>Fߗ=s,#rB3&>B?7>J mߟXLHL2Y7sy-w{;<ܩ򻕒pnxL{\&|s`(/9~@R"/k#;t؜XjoZ4UA1*XKH[Owoj ai?s$"RP44NL!b@PĹs"asM>Ib_?Dt2ɳ,Y]sAдF3 M8x [." z!h*1%;@MHJQ!DmXn B!pw66DB`^W66*TꙀd(AAUqp>=qP A5Cbbz[V0Hj"h ȗ]?CY's )#hk^'cij*`ʜt[U@+޾|iLŮOonolSY. jYj"#BTBNzv*!2JcDʲ(i^Љ0)֑ɞޠC4; ƨ"DԦUH.ˈ6=Q&/~O׿?'9=cr 1p\~~ fbwsݑ:oB`%OFm U٨)?ݿp}<80zGSF`9&IAic/ 뛪 W}T[YOkk ݸ~24F$e{#&T:SP` `3+v!+2UhYA`ҴQZ2"("aiω e6݌1>yFlJՇ/8L$ q$`S%0 Q)V{?q_#F(T@I%1UETҦ&BC":_N>OX="=納MU[9ٸgKfue F)Iq#S䍵f]8 b?wGǡ^qQUŽ-DK:ef_jQ7#?|%Q_?c'9ڶu1mw%6zPK//~ޡݗ~*rlGݩ^GOFml>-q-j%>7pQ2tF_Pm[3;r1޽'ow(fH]巁MH'&Ŧ1w|E3U}D8|;ă?x3DHЪrp#4+8& @dNoTU8| 3j TN6Pd'7p "I L.j#9Cf^Ȳ!'f8nbC}-UbJR~TfXrs`)8TEbr@Pd3h4nbUm-U6$( XY++hD*)\P9sXFϝ?ͼ&dy&\. [x2Cw^|n}Jlx{PC*ALߍY:b &6m[N B X-Gf\\WUCsQ<h-h[Սq[B[kD"S4Md&Yfg^ƦjC L ,BQ( ݓe=1Wnòtjv~Zjfbfs yvv7 !:}zie]{Ͽ}ưPE JHbmAfh[ ~F@6@1vow"aGN"*J=9xK:zSS7%ïW5Y>wЏ3{ fTCXΩ8gb:НQ]Bc/}\GM; gBb8d99ۉh8`sb@pF@ % MMV/zf!!3#3c")vAp|$PFd1WH6N 0fGDA#":3uD8{j t:vA+`h {RQ.n}<͎R`3 7,'HӤ &dI5HۄiCrbC붚ugÅ]3g3",7061}[™ K[ g-4 R)οtscúpܥᏇ\~{'wꦆlI YbhŤ}XDT բHtyT4McD`fmMu+ ޻ ޼qfpSr&Y~Cg.4zws׮w!3睊A@MLŐy*'5sԑ9Y=g,&)V7aDoڹw}gުPLM㝭9e%"1 [jg/A`G?K<E=Աрp:. zoK MDUk(`q=8Z_oP'e(B5cbDŽsOׄȐTsd =<_91Sg^#(UP"($D mq4yQ:#!:j{39k Dc&/#0ǐ{eХI]H FMhꠢ*mںi7$9#lwUV.QHzZon A:< 臦ЃyHH"UEV\o(+lV8[YӲtE}@{*ň̠(h7u b{с0Y$2j**wihT3޻zD\rΧ>|'•f뛣*{!ZrBN0Om?*)*Yi Ȝ@Zi?IJ"!0RW4,G syf?~'i5F뺾Ъ[oɟI8f%ejS.\mz.aA ,T0g|ʷw$q_?Gs#Z|i$g9i3Y/,]eDNʒI24Рg0ǩKT:ix<+I5;P,h|+s]"*hXmP5‘YYcLK[ R YִWZܱɝS+3m,5Ef´6Ӵ `뺚NѨ"X TǏzkwwv M|r@`%)i0tnYU`HUfm #G"%s4B`Ǥ lLZEY>Ȯ$.m]䓏<߸Roʷ8eE9 ꈬmx" 6{l~yޮjP &"4i4d]Fx5̪*xjU671Lݹ_f 换uW_Uo:>x}Tvc}= :r Lr-rsι"wM]Ff%U2E"7R΂|78kY(Cx~_&) $I)|hhg{>+zC2mn?g(ZXF8D weev<&WT{{ߟM!rdt#I/iVMn|-P%XKN. wՓ2**D_?frt|׽3&寖L&+vNShO;{Wv/R@+ÿezmo >n_ߥ[,evՃ4E;shҙ._Q΢h9)O< i/s̑Tsjns]ܘ^XtѕP#->,h.T7=I+H'**:f6iVR{"fRpbɀ9kICb(;R/UUTM߉ s:FEJ<_1"Bj|<^/w!:OQ#F‹o@J@UUؐQ (ESU(Q X|7WkHdU KĄ_ׯm^_s@DV.XgW\ݻ-{ƀ^AȡAL cbYU{k[ӣ}dDzZFD3Z}c> u ?O~W~ڔ3nFaowg!2\5sj,d%Wt~6m)pz}WѤo}qu:HZK*Q$rICS%B2(̑(2**yLj2 qHcGXiyL diB<<>22#23(1r0*0w7^hf}eAE.齝ѹQԚzcGH7]?篿d5Y,! cssM}|0?ym]l' rm >EK]GN-]I;?ђ 4Hܷ^uÎShgarڨ}vNa)9bl\|c\+( :GhjENn HDZBm Xbe$,1tYPX}"`s[Wncf"n !FmMSͦd:{EKh,RAP٣El_Z[4y K۪HYhbq#*%`sĐ( T%YB5˂u&9@y՟I4"vu EJD\Ǹ&ӟGq:(HY1X$kQ4pc,'_MD$""* P"y?:A׮^{ڭo=sA/\SUT%6e&4M[` heC`bQ]f~O_+10Y-iC'8~~W|;?qh]WD%"YD$J4@>/j?6G4 DΑ ks]VjETTq&6߸hϜ˜sWz2[OTۙ"'z=w|-TbT]}[yg??8Kʕ++GiI Fyrqw)מ,01 s³v`lhgj״J.Fʯޮc'G^g* 2g(iEuI~OHw~cc58͸\C9\16+z0)]L> 3oU^ r/̥ 'DL(]vMD&)v a QłNֲ&,TU1$H- {E8k tݛ IhM泸s~f vpZ'f=\M A 4BB9%T FWv~_tF;S(h 񪀝b@`G~Yh|nhۄ JD@202)RK^7//M[d2>Hd}D xrx8gީ6 UF$Nbj,A6Y;]֋BGk3fu#YgUe{}k.7HUJXb)"Fe8ʲL+T@D[ ,yy>h 05R7U_ /Qѓ:BrVuQHfG淿su׿xE?w1Eeֺj.^pJ5gʧ.E>pY`v4rodzYxIp,d2.O_&IYz.4p8\[W-vn|_/Y y'D0J;R%1|K(nZZ:=Z8QRT0!cR.DRkaRo IDATPHscH$"Iɴ9)J#@ȭj 4`u.KN4LQd}{mgk- E-%p(1BGR puED@F&f:>j4M8- fmcWME"9qoヵ:O,Ś&xB! ?B6N )\ ¬"d^"/k/z嗿x-kW7w=|09:Ta$L,oʛk;Wvv-p9{ XWYYVMPպjbHًSyDRl&*F ֲzP#o4($jur_;?I5虽BDl [9TaV84u~19:>?{Z?)Y齏Aj3 G]YOvlw<,1bMUE5qYΖs>sˢPM]KN\,3ݽw'qZuASvߣ;z/09<4Mt4)?=Kh2?]K-gO_&pInIK .}9 /.Ir<7_Pq'!-]9$CK//SGRG) rqW^a{w`ᤜխ᠟Ċ2bL&O<&)!(!$DC#Ru9@5M"B"cr6N'ƹYU! ^>S6HXV.7΂9p ҋ %G 6[u.-@>6>FN;E]\gg/(Kd;\=@ƫ}NU:o"?s<< * Y,,l-"2(_# 1\抬ivvEzZH^`FjDU%6||k_oߙT*uUଙWUYF"bl2Mi~)e&Ƙ7W̊UcHj6~n]@ @UYzATpWQ`$%&r}0DH|ǓC+M&Vՠ*9dâgGu226 Oh'vY& >xk(231.a%|{9Z.yQlņF+D= w6Ūish{{{׮]{Q!' xÑ7ƴ^tYp\οy M'h,X:1r*\AH,_);hK2X~YS.ߚx-<σbiVTu*ژs' R,'c.-t"4tSr ŰsڍW^ݽ~x:ucBc1N)B\PӦ%σvH*s -kSY &|ՎII:g<Φ75H\VݭOe%*Dž<^=Y2nvĻCÈZ.+ PXV{ _UyK8DCdLExrl!\yMT; N|DDC;׮/Σ{4! l1菶D蜱!`MƍGGЀllo^{l<ɳlk{k0+jh3;Gr@R/_叿P׮q`YEE{T@ kY1 Yڹ_Y߹{㵷 eJK;K&*F?~{)Eh ׊-(zj-Mm6Lz7Z<l'#ͯ|0-wQ[_o~_hc3Z Y@s^w8˯ Cc!h[ >!0 ƨw޽;Xk^Of8g,/ >:11Yl02kp8l~K_W_c7tYχM6)?~ݿ_͈1:^5D`-"HT,s.sY,몚# c,ˬ%r2+\[wֆü7*LfLf~NXZ2GDxxÇeYeDY LBT ԺIօ=NNB~Y?zRI  d #"1rYDLJtd2ۻ Uyqޓ w88z;7GG]e\ ӆS-.'KZhNT\YBi4˹'_K< 5?Xbzp7]ep9DZ!c a1A޹r6wvXxz|2uiE ɘ9є҅drVUϲiӯ l/*מ9GoWwo)BXg| =Iz<;X(UEw࢓ٛqœ"].R#](+j Skeǩ^ĩgeyANևƲ`r ,޹]̪ٴ!e4ƢDoPAS(E+&ni#=&Gh`(,y̹,3c"ףƶU> Y 525U"fm`Q /mf,i # GPױjjƨ!Hp2S 0:~7/vQsnwkgs{֛oo_Z>ڈlB8>~ dy՗;wQ MhZD %QTEL5r]Uu"SՂLAUu!emHzYsƐoBSGE un{g9GdDA*@.zyf"~N΢wsg!GBCõ6׾ 1*9c1rr4 Q36M=-KcsYk[!0'S|dY!N&!M݄4MvRD24X.Qj)$*Gd\zh4ʳ%60N d21c.chheDʥe k_Fsd< x|´]p'v.sqaœ,i=ig@9sv]8M\X|B':.ճB#F*hcl6+1XgޓP!"QZ۞n@!5W4Xp"Ovz !cY,#kq&]:&(6LN2F S8v'I:u[]S "76Zn@2Rn-x0P+h=/xywhX6o|Xsѝ}?>hڷ~ݺ6y|!i4!kP[[믭lFd,Y:.K_׳^Y$m6 }ߺys}mwfFY*!զpm-p! )Yn-DU&c%% j?}w=V"An5U;FRU!ro9KR"K5m".tb;Wu#-L$s/zɇe6B._,H{?NgY>`Y1ƺ|O.1ooo؝7owr}fK `ܟ<ԃg lz&jsHadNJ3Ots|+kx~re_,.hceX:7_YChL}7>;~p0,MQx^31 ID`iHUU8'Ѱ.V\E@R Qa%9ƫuiin|c]fh4q}+WO,A.Ltaq`e?wFP.bl$ܠ*]. b xY8bT,;ݻ#"{a?wΒ%PmomCUO3Š<Wdeq><`t7_{w~6wHe31_z [a)GO84MS5M>cEX\Xºa!eF%bBr .~ߚ;w CY ef0Xk|UUfRfIE&" l} @+)W9/ؓ)AK"E !֡YɋZUCUUUS/MƓ^x˗c,|~sGWvB@^\cِێl_T0e~1vhK!NKqt(6'j-]dHL.z3>Ϣ:9P;}ͺW>qͫ<գ/>:'U..T$oo|>~:V>F A9c ",괮cUDRDU!$Hg7uBiԤb@TպnXrWEQ>oƠP !~lgg`@TX1ҥ.)JBS飄-q8M)Ypc h{!y~qFԔCP'l}Uͬ7FO?6M`VE͍̹lVl2cfeeD4%gu cl!Q&67^妔G}6.'$ FeFP 0hTEC\$@6zqgY3n~wy?-f~A !;:Sg=H>ƪnfuELfsƚ" }W@{ִ doﶶwȸeJYUfMcE$ 6y8$f}+&՟ܛ5*hr9 +!G`Foo B@ATc tEڰT,lc]wx;+̜4vXLE;^ii]=7~t@vAAÇ| "j"۾cljr4.3kld(0_UABdfUueݏoLJaFGya\3Q挪5ì,D52 0 AfFA㻟FhEJn JyFx8 ӼK2Hr^K$\Y8cszc1K l=a֜ex)9OxAy. ՞|r7~-'ʧX7?{BHlA2#ѐׯ\ywNg{G+kA/+2CǠ"FXbdD̳Ȫ8"0Dt?@[jWN=YYSh4eV `͍lS~媴^.:?Y}U=~ާZdB#"hj*F3eM^o}s ' G<3cMk1D 5DƐb٢!!sVਚN=fa\z+!kL~6*y>j[/CHj ,"" dɽ=1aν~|52^ * [ hU-B\uH0@ROKÊ@*L%űI85"2N8FM2KaB:c3FmPU09CM^Zڽve}wk6:0#V=:FZ1uZˆ(TSf1C: ?K@"KdغLi٘R2Ѓ1(W+4TP(՗se|ik??D=cO](Bm|׷6ݿq]OѰgZ8(MD2bj#k: %8k,QXeb!0h6ʦnYeyn])G*(NȹKj> w׽& c%` [SElȬJ,mF976F7f]mWϵ& { h?~GLgFrcPUB!6lM. ___>4MAd27Uc,+z="^Y͜u)1_LA IDATB2! K4M_ C@nZÁu`baYbc\Uc3rQ`b] $jZJ@("Ax$^[h#ambf]?w 0FL7fsZ/,AJTY'*3j,L,PCl< 2̪Mn"2k[/rYU׳$Q`Aޫ,&sʬiu]׾Q"2d>%d E !8Yk"үb_]'̜,OuΑ+833Q\8Oyw뢞W1ROg^ KK3td WޥW+O{!P"vVr"'νrsvy)ֆWoݼG?|<Ϯ_噵@ 30:dULAX,FE4#rTU,xAE94^|&c4D R{7LoGUb\a pJ":e H̢A.kE$?EDVfePŠ7v?dCGuhܯșuQ/]|yD?_C0:RkX Bcݔ,1,3J[ =z,*HH |,ehHBM,s[[yr*B ʨj,bP"j6z6-ʋ(D9Lj~4F_QT0_x6Y٢EPKݐ@ %khc`%k ,18 kч,l:k`1Dn'bҗcK_n B0D噰*@14koi3k&u-hg٬X`DX-FR͝7^5 X6bU*䨜l3p ꈰ`ыA$cM *, 幱FU?iRG+]ӶW |wyN9]yt6%~nrz^ K3N&>M/z=se c].ʜq!DU.2#P*Yf 5 "*d(pbv2kؔl)$Bc-I G^OXZӆ-a-EE@roiYcDf!ȑYXP"dБK1s (qb͘;} Npj` @SU?O} " (U5 QU%TTBE2`y}2DFXʪRhcc#˳!cqY6c`P6uUuQd"cztPZ3pᰮkDҦlQYDb@PU(1Q4xDTM]ຂAAQdؒ 9TATt\޹uPE@\9 Z `JF;(T`ݨAD" k#)3`D l]RpX,+8}ɷ9h—"hccc47cHD꛺K!DCh{{{M у 63`mfeYf"i* #@ܠWlonQ0¢R#i@T1dD6!zCW)U)36*G jHd!P,sh T@C|ly ,Mq2]kQH91p{X+ FA"vr*y\{`F`iYM4HY}RUU )ڝON&~Q W G0iUWna'ީGPAԠ1hPT%FffQEXA U@S !K,e<Օ{ ]NI?`K1,.?u ۛE"z% aW',<4e1$ŞrhcvpY3iʺFj'eBq<{ArDtz`/ ur<(F46{W_zwfͬnnl U`)P1C  F ʒ2٣**DklSץAK9ԕ48h 6>|+*6ɩV:J",0YԐHJEOVŒ%xAbQge4d ^|g)*jz2yկ~)D뉤&Q ͵;Tf%$9Yf76GG;Ghc$Ns #Y!*3c !2eAiJM Zߨoͬ7],~r7F Bؘ kH#/qd# @h,DF]9 єɭ,"ү_~|~!p=_HVj DEchclԐ zKH4 j2*LTȗyJ2SUEbq8}UCyl Lpa֭#[mDC/{ 4몎;ʘg!BQk3 ?ְ}?<{Y+QQĚ*T֌gI D,FEB02ƈle5&2,R+_XqY&Z>OQ#@rL;(&NJe9v\tY1 Iesb= dG@m顎W<ayOFRB#=jRYd^Hz2̳g~߾`8|f?L5bĎ 1J@0 -6`Ri33Msc `!M]դ(|{rDlـDэkl SJvI2F & Q$BQ(Fj2%RRgܚU}~21/s w!iGcrpa kL;g7/_-l2 #*8 5ņhtk1g5*z0RbHf:t./r? ֻrcsmp7qmPXA>ͧ@$k+h "b&*%*83s.f ]!Tt~*TQ0Sa",1 D*ɇh`"12M4BcF?oyVM)жi @[t:ypT|dܥn-1˲zLU,9~ V|u,?}[|PZ @m+pXG)ӴBc@$d)3,ѴZS&ɳ4ƈFXpCˢjvY,Ԫ(a2qd pU W?gÎ1E } oMi=eoXLmvIXGh1^Et/7WPkk_}m}[ٍ4M3ַ^7hDS5٭Ss&"3'ygfܶAL$jӶmSM6yf=!2"f&j*f:<ܿ9I 8=ur*&ߨDJf1")ZPI\LLĢh !&jrd0J}fP 4)g3Cӽ~wO?i3>t(#!Հdjb@$&ydWz篾rknEჃ=lȵFDL%!4vյzΙ3[`Uӊ9>p8-HH @X3;.NϲLF,T"Vnu{gϿEozkM ` UPf*EDHQ1~}f3/ɤn:)Z? e29set:搄82XU= 3̳XQ1:UIʽgv(2c NƊ14E.cD0zbş+7zױ%iޕA%"ibѝRZAB1(1p‰i W8f2Ŗ~INy[[U69PR-;9۲ac|*£6aſΒ}ݖzy XB%YNZ4q)* ;qe!֏t<n/9a,ibx*@aE?Ӡl1қgνyݝOݧܦnmeK""2)*D`bIEUnf"DABc11 3+&.NGݘhT M/~qSdE` SSbuy﻽s^V|0ιKs.\;wb2OںUX6Jk&Ė9l94qFNw!xRI4boi%g.?4wQ"8nt`(QUDzE MԅTy'i'&?k9EPԒK&΂ HECyaFͤM9eo7:ET jU mm0+""h4,; .}) j6쬦)PQ $6Dq 1J$f ?R)_IӜF@Ƽ3^)3я3oJ*UF};$Δ'\W-SGnE䮲?y'V6O7oa%ƊBWi`Ԗ\g[XO^Vy5~\?N0kӼ)u֯~{eڨ>d9%YLٻ2DQ(%ɇ]<$cFrac3zg8dIU%,Y" h*H'7Drb iI(s&`*qѢgjȆHn $vn5iz*:nY,h5({DTqeX@ [D2CK{/0]}HO]y(Ãj86ACS5Sc%n]@fD,s.D FS3/eʿqcMhG"m%1B{fjsVu!g;N/YD6_X:]|J=Wz Wȏ4T6֥ﳾ[o-#-cXZeQ%r|nNZ'z~1w%f͍sOGg6kk90ft읚Jd"!8[cyŌjS7xR933rjd5P6̢O?޾fuo6ťѲw B؆64u6mbJhmi-@Q4&~թm "cfGՏMI2404Su;X[~4:Dš;贁?;x>9 "@\?s.GC#`]ARrID[^p<g*uӶ))w(3; HXJBjPw+WzWbIm}kpvk",D,*@6UULC`h0%}Fdk;V10ΩEAU!#EԨ2U9ܘ)9{@PUvHb@3d}`sQiqo6QQt&@,ʍEMFĞ0(w*Y@=-F): Q۹_`3vl*u ' VMZ3L8ћNsɔ sb 7lh?G ԂDѯEfƅXg#)2%#م0;M[}OJ4%_57,trW6Ng/uܲ:G53=e7*4۷wIrLxG=;LzǀqtbSӾo ʪN-hWUWu|t6+;vʯ6 t4/8Y9nr:s:Gsqu`6s8Q*'lyvxYПz'6˕NC0[otd7^|ja}3"ҙIDcjfSBljQDDɞPq @ ISvx315vr0@#2Ҕix@!L61i&ڦ6Xk `T̘&TYddDFD=x3dLČK @o|QSUzOL(='}E^yY!^[s>7 vZ8ܿKTw0>A7+:{1 61cm,󹪩@(GpsL\ٷ]hx5kpdk`o4'2ֆs# "TsDya1m;E\6H0/m")L S] IDATA,ϲnP L"e}pytVTƠ׫asƘr3ϲ97mx0F`r9r>*6X# ۵edzrds&[hF gn})f= =T$vz4* ^[״'ˢkH.hVrN1 [Zp6fEGզg;G ͞j×c~ܤ4;nQ+׶gy~ݸ?Q5zݎg5Ԟ "N I`hNmU9bVǨTј (mAcjƥsΨѺn%)zD@{Q0:mlj ?EU OhVfHp坒[ `F5h֫K{* 13c(@H03db؆e<"C3J\F(b HjFHl0zp'b@T@=i bT`B$sFԡB݄ر* hώ̉Em:@E&íS% yf-;b`E%*kMMR'mBzj~O%105-!z,*CP@&./:SkkmcQgtF@&z.\FA'$˼˝s{!(&%9ff@bVSQiQ8gT@o~oc(+=2;~4T7lx҅?x,"z_eL*b$2DĽn 6Mu^(cdhHjHjZ׍t;7sD\aQ !FGdSpx*usgըĈ`育Dͳ_\9hĖ2_yW/yׯZ(6l2ܻ{S#@1laC5Bp0HO]{{wz!ڜVm'79Xv'T~`EI}uKTU/1"!š~R.4Lyo&L -6H.ꐣ.ŦƉylJzRϓ *yqcb9e7񋻻cSbΌg Yn!D3fu}l+(bqUya̱Uͳ @FA`[[/{=pRIb E:WvHZ&}^d!Q8! %~.UU_723VqަǺ=SyA¦iZ` U`,i N_ӹH籴bmaD<{Ϟ?;{_q4c5ȄĄ{{=zi5>]28:x'ݽsCC2'ǿZD/;=kUeL4%;8D 9 _>sg{Cwn&MT[Yzw"L&of0}˯}ɳ&B+k"`.!ZɔP2$n{J!C30}LUS@/sf)&2ęQJ]@x:e'`XWm(@crnto~bRirɤ08<ؿw֥K"M7:( a\@GO_~CQ$$uCC eE'Ѫq}x8sΞ٩LN,?hm5NZ܏济NO+?^q;=D' O M%&RΧ'!R9fM8q^?#=NlNAVHf)m8UJe#t-;>nuWQ"{|D.b?8j끹f2!CLfQk D(a69wf$=hƣ=Yc1{^~Ens`$޻|]81ϐ2mv/R5D(ֶnZ 010#`FOy,zͲt^C;U!s4hhR$#Cthm@`bH\&mHѤM{ wW0Gl1B ( f ,ZI,J9B3"͎HGh 9·sjxe\ו6;ewRUļ7^yU;2m݇Mblܿ9К^Yf. A&6w޸#v LQ9@,'}bbU7\ܥgGژ3Bsq4<{#8L\^ЃEl0 'DIJH)eT3c,7$)L&`&䘝W `eۛv$vjѤmC/3_6D"ƄDMQ=5b'P=us )ݹ}oހv;û[ yč+s/zz ,mlc sI#Q{热Z$K_KM*+yVVD'*rYi=QfmNByeq,>1*iGx*x1ktKWi-LdԹ,t500CļWy ]@42w.HQm$B#|[;A :{>u:B̼+KW15e@TskA}OhYDTLTM  MT  BH{@!bQC 5P4""VFsYB^HcSHg&"*R^Q9Q *Q0+)+Lk3Ǝ^q{TU:~h<.:+=7X"Cߒ2;FS0`3q TSvι+W_eÃaxD)YA=}D +z\kͽo|;7}fΝࠞesSϱ{M&9(XG ;?R>|:GAu3OΟ;EyM3<T|iYt7667>Dt *b&@ ]T(B -[w[;g`Dm~O>xc`K4G.m=l'~wy:kEYIA"e(Jm`Pw#/ל~el{D .=듷U%q 3ͧ4<6:GXp˩ E"$G\7:4='|a/52h}9֓9r͕g)o߼Q6fQe*ж92/"&FkR)xn7}gFz(ۦEgf :DeGg;閽n_f̉"DBST)/$NEEQR(fDDdfĈ0MDFn]WXdH jj8 A5PHtQ I@SS3xGލkkϼnF>˔ZlFUhey"p#č ս1L|mcsfgswṿmC !R 14A^dDWY)~Eё}Aد4!-6Z872M|6@<ov MU8 \uw@y|ȗOkjH9FN`ʫofܛXEx2ϠWj[󲽳^t|Wimg9F"" [R IDATw8ĨĔE>}ft0nT (2LYE5*BjS0;@Sl Ũ6Q[&H5 Α}fRR yQ E$C,XВ$\fE[t0+ BI@&՘Kv2Ҳ̘=w~օ[zʌ Wt>3_vmwour~ppQhb Wn?KóhRHiwᅗ}?(02ׂ66Lv.]w{K+("2Ai"Y"cCP3P#"3Ht4M˒4dHHgx8_;ݾVy3Gme&[wLxc !;E;5ƠSi`BHC+.4HۄJ%ֈ""!:Gm}[o|)򧟹ԏgÛwof]EL0%] Qt3np{p;' LMԐ@DU;4ٻ}s rL +I/tOb<3c5/b*7k Iişe dy]\Yy%t;eLA3 _wMzߖa\DCҗESPSasfg/onͭn ̲,WYn #4.eI.yDr<1Q39E5Q@M" bbbUlBB$4ss%eʎpf@fH\HȦ8 7GO0`C:‚8FHVBfdԵUagY\q 0͵W:xQNCoc6z%mQwߛFZO~n(rwΞā0D ТAR:㧎#$Ai|ưp`XFZVt\h4sN=}&:k<QCt:U \ RAe#KB `?FA{7t{"@d1F &sV"oܮڨ pz#cA?qUnf%)MæF!S[C|Ob[fw4J#LH :8VSO}ɛ{ܦ̤-"x'h3]w̅^tc ҺeQ#RG[Uո \zc:qbJhgxo^UթYXdY=d]D؉4BڱuSzH1ԃ\hug˻6sY-"^ $Y^bbl|Y81] 1p1v;nhu [FK Cz0<F^(SN wSx#Yfc Cv]j@&D"n/[;e]ksH , 2F5eA$dw3xF}mG:q/[A)o3 Dwc?'9PW(Y$gD,8݁LQӍm VbhxDZlSLBL&}:9-([ޖ!(0^ܥGF׷wNo-v^tz]HΒs.(j].8 3cV6Mx$(" 9X F ABEYDXc!(7譠 M&&;9E4?Ch DZS&'i؄{Sh6Ϩ*!#T'$(t1+LU);:}zý/<©'?z'ry{k{~?:TXp?UwQ "%T/!@@EoG?_a_F%P~`Qh7/J8veFR\c fU @\SnݰըQ:gY1s=@̹K>lv|U YM(G>2GumnFJ/Z,c99+W~7橳v:0H$F.Httb`\ z_VUb(S)d W`gR.5|%jfc˜*$i"5*Vj4Z~'8t/2V7[ƪn,e@b|c4[0NfG mq#;R<հKPC#hu)\auƐTBY_~ӧg]λyQ.: dYA.3HYmf4Ϙ1&#L}@9F}hG,IXԨ 3$SY[/ 8`V  ,VP(jȈ(@/#O,1 EHFp Ӆ)s<(NQNsO׏p;{C15HdFۍ'ݻk!!~q} KX3xg5^2o|W޺ro]tϜ> 6'MsCQʽw7? lCg?F FC<ƈ~ omm YK"qƤӃ*ˌH(J'Μ\غvLk2* /,֖YF%Im:9U d"C):\׏^$˜̜tAY3:ya >˪)bD"Citijk%e;Ulv]aYyR|iL,d2sj\]80_="Ըf/YKR%よӛc#ڝ+n\HK GG^ >V|` @71 k^THj Y>~OShH(6H#"{;ûw+l""р>M`UAQLf<*"ek;ϯ^yɗ~|'Frw_yg|λجb,ܸ~uwF 1Źɀɥ[ Uj'e]zq4փ,G6?rQ< wn~q76KQV !@dݼ_P7Yzl_us,H.(E0b9C!x"t֜sNo$ع]˳ 12CB5 1GY??CJ۳e#bY1dHEU D<i7,y"^ƷbCC{չw,ǸaOTA4ti=@s-2 hO\_;!fkýU_x?Я$=`[> !`Xu2i.(4 Bi)1ژu * 30+>G ,>pYA !r U1o "&8~()$d䄔!EEeIH$CADJh9I&yPdP#JTčzqJWb&k m"#k#;}.W^QYbYVA^*\P!@d O?r_Smln8s&APBǩ[o"E6CX F`K鸕BeFQD I!HUusÃA95J`,92̱,_+r;ڻ(jIQdEE AkzK٭;[?,[W6C$(915Y盛DCTM*ϨJBdXѨ!n[3GftUU )J<b$!kAa8 [)N/U  +QNm74a,cɍ8SLȔ't~_wVe?5`I%3ۿN$vlcŴU;}"η*p‚WeGyw-8 !@M;v2/ ԾؠX:sDBD`_^{wyg%pE;w2$eڜ[rYmo:}r( 0Spp{S-H! 4@)nr l ΁`sgL2m<ʁ"1Z5 <8^N7ܸwk?ڵ `TCdbcJΙ2W}UUа@]ycVUFvͺ6r,ZqEABY$@@3yRADcmeuغ'h(1QZ_Rں) "h;#H 4I9tQMqI=OT.V˞)72,Tm9U;7178c -6ey+GOtj<Iu(q(,YTЈ`D4^ƅ"E"SE $H ͦ_TUq5:3#eFj-"kFI%4k(5bmq.+FCQ4kE8~9҈<HA0JDd Mو }/ Kjgw-h(Ͽ;Fʠ$ًG;[yG4 2`;%ώųLfLN!k ]c\0.ԁYk-DE"4luT}?~~6/+5ff Wc<`!G\"٬(88rv9FϞzSOS"1p1Q1)/Ds!WYgu:˅1HHlKI iZ^eBAilT<:0Zp(=.v./ <ΠMPղVBohܔIܧ QK84}X=41ce%͖O'bZt9csOЁ=jL뵠%*-h0p }1D9"RGfQfȒp@fkvj6 7Rb/T lIZؠԂyK*hL!fhc`Ҫ*QH ZɎAa, 5RC\nyk5 @R|6NΌ BJVȥ֨WӧZuY(1S>3jcOqdƒ8?xOy+VwP#z1:Qܽ>xusr(I,b9o4R#Eɺ mFbfܥ01vM?j}qj| *ҙVfm#h6ӌJjL7 \ɥttD1~S Tw8DAEeƺpfV:H\QKKmX;wl}N` i0K>3`-Ҥz`JC@c.sXu"3|R!$[9Jcr IDATu 7>GqLTk$?ZĄi[ H*ȌI񟐀Z*P#T(<qI%wt8wF+? sskwl+nYB)t,Iv`ӯ*`l܋޺{[)"dݧ^|o_oj4vh7^8s拷O,'~7-Yl mL{M-iMWwO9tHoi6;ܯE(ils% ZӰ-JKd:c| AT%},BC"ekYc`V<ٰ!Dcl1nBMPY UI5+Qژ4g|xKިb@1^WŁkX#lx7}ϸ@#Q9D"5cAaKO<;StN0s3~kڇ峧ϝxQC`Fag9Mڼnl`bJ,>\}ﭷw7\Vi75 ""!jeQ8yjjn\v){? !`]{fƯ!D9:T/w/΍mcyQMٳ7>*bhU>,w \#D .&C8ĵi3iƅk, G.~rKwj-X47e̔ߺ 3Utno4*͵srJ[`2-&Xsp,K%yVF@O84/#*3wih# vt\)1D>0R"C] >(uL.@$*peU;L{O S_ " ݜ&'SSM;V_ SέxP ׂ>)"tda@54NBVFH:m¥ &AFz^yNGc+~7ܻjHYfndo[جnllpJNQ,r*y^n;[W{mFΙ71"<+1E$H$ry(W!ԒXGWT>hx'^xDDSN*! =Y 8f6i*=RJ+MSSuۿP bس2Y-Z/gt#jYL-V$ɦ9Uj)<[>%%@Ӽ!J %^9z<8P{.K_VUeP(} h " 92HYV }@"5VTYnh[cGCg"L1E hm4^ 4iimYk`teQ1p:Mzu8*8xY]^* QBGyv Lk@5v{6_֣Νxѧvvn][73h4 Rr<,3E^2KzO?:ỷz}eҋ2cUUi`I&*@*Pwm̐$(csǯ+Qt{6f7)jgq~XGl/N5 }ZxP40Q+T u-kO hJ>XcA @d::uPsiO͝cШcxGrTG9֣RѨ(1U1"T6Jԑ/1*{~uG;nV@mZifaE\F.~{ѧ{,/>|On 8g6Jс{?ş޽ov((uC-KJeT[ Wt]7 }|/~v%.abrd~rGѱ{N2( Da߾su%~\jr k!"B(9VaLf'$" >,@2EYԸ,D&>D! ___BHϾVz0l*i3~lK E yPZC `$ d 0(( ( 0 c4Wɪj%k?j甚:ED G8%Eie|3pcUjGUݽ'onc@ t"%jHTP}U_N|r4ur$AYN~Pzuqѽ}(zݻee`DT c "( 2)gnjw*`|){G|@ii[`P._GP PF [n|'^x `扵ƍh|CkfeE0HDhb[;+$d}H{5RDd1KDaYW*,c=,I"uX㦯 qdW ǂo\[qyGD/KM7;T?Lf~]z֟W<f:dJ9~8{jTQI踬@YWKhC\YW? O:u7^-GU ƙjK"#ţ?͓'ͯpo`u8}=zE0L@m/SFV9aw?q3/z]A\o1Os> Qk׬09zpr ):ݝRVXbQ<' }"|)$h" *@cBө1UTFAcX&)g0Gch,҆T2(D0 IT3c {Q6@ e!IPաU􍛤W9UƬN y^8gX*ٚ\6=T/7v{kOC(*fkUͲg{g?|a[fS_{4`M i1A [k~pZlKBE*C\(&] ױc5>D, 1dZWu"FQlEc>P$@Cޝ"YJ/$G5u"Ƞ@l2۩OJbN[c"GUAd@C, IdUl}|t_1/,¢yqU{]K=7U> F1|xέ_O<]inOt}-˪1F*~7|w.[[߻.]w,s7n}Ec.~ sK:wn\~¹g_~DQ!Ϝff"e9fzTWXSfi<[qFS{e#Ia(PsM̑9 2$cUDTH؇vR3S'26h\Tsž+4J`nsYZu<}cK;)vY6ONտʌe3@w:,fs<\'גvh&+TT&9kL86m6@PĺٵT22v\4)}=Uڋj, eYǨUCcYEćXU8ƨ<ϳYkISvl"!d< Q*D_m""Fh"(Ɋ0bL kET E@IcD\UjN@Z`+LBLaTK;~쌯uroc͈m=:ؿ{/z6D,֐!\^8t{w_߽u/~;+ ɾjp0{_^{6\ڸp3jgSwo?__sgw-QU.]<{*"J#HOJ?1& cUwkގg/^hn`N9ēn8#6y.VuK VCTVQ5D1(~9U 1FNQ @JCU`8irq|y4vE:os/: cvnN-d3Q]Q& 8.McB[*DƘ@bQbahŪfӨruf\CfF2=-#`@*a:%'Q?  3I 1D龍) BɗpX3KYuѨ EUD!CUU 3LuEt"2k6)[cچo Lhw*m2XbQ+%r !0 "0EkŒ4~'mq]Яu, X[UY5WF[Ν}[;qsf)G^}ݽc 4l>ݽjݾqYkg?yYg-9~.WGCKh{DC}Ol_9h(;+]myywhws'PcSBvT#@,ˏy? ;Qm6 mߺ}CDY$/LTE2Fw)'KDѠ8eE%JD$9N(IeE/k!Y4D(.UUCɯy9FTCc mlzDE$TV$A+B&;k_nZIfª>{"6]X lݸ~gƨ2Xcڹw"(Y|彰$CM9^y?u.sY(2g3g:cPGׯz \<{n4 Q3g_ XIiy1urn}wO<̹KX " `Glm}GD"cqɲȋ8 {w5`]kdFCݵ"+Q:-CjP1 ((2EGNEDD9cx|Ip/xN.N41g`61#9f0N3s a3Yu/ttnhmP-床]vyf?8{ZVBH0X3f=Z6%)Sۧ6^EuY !h2*}Q҇"BLR. !!<F}:EYȳTYT1UUTUVWZv,"Y<˜3֌D&$$H]DYEE#I(p0Zc%UIde. *2q D{CP96 *YK$,S7 [pDV/Mčgnр&Dn>ztB*7UtvZ[[kA; 76lT]w~۽w>uD \: kAi"3I_Q7˪ !2&8aAd_~{z1jPޡH!pd@X|P HZ_H #  KV?Z笳ƫ%D``ƇՍW×:@me9O/ ќu.6ar,(`f,OWܤu9oX`cip-⿡uaJm:4>aW:M:1,H;7 (.7̧:>޹]c0t{a  >a릞8yEiNeg˜͜sYk(=sQPUfcB dXAic'h̘12δ1ycSocL%B`b6G˜FXʾAcҋp-ާCeΤ$MPF2h G,LUÿ;lec3 ~oxuJN+W;76~?׋;_}~;!x2|kٸ{,7?26ɸ;{ĒmG]8--6=N9oޟ ځ&g\G'^˖^B0$\)'٫by1xGn>yMh| !DXHcE(sUUviWvjV^Jk y繵kdH D)"APfi,L #̭3>44,"4iGMbe"!.0:ncl/3=lbUU{>ȮmqoƵ|k뭍+[pYulpwQnnzx[XWX@'?Ww|y2$Y[oppֵE CㇾQQe{O/>j^7HCv+("Y>xHAxe2g'#tc5(?~"H= AF")qd6YƳ Ma80I~Xkje(|,ot]gNI0(+GCC_Ħ4oC # TTyUʢ*"R%.j+_r+:H[7޸qVfMe{ǝ{ww~ʭ3A!r9B_~Yze{k8+G$8AQlm7+0P$\J QChԇG+[k1~~"K7,ǧh✕5!P@Wܽv!ee4h r7_+o<{v=U'ýX#Ru09UAP *"0XKƢ* FHIf32+`0^T9mlj2dpAc/O}LA:]{mtwa ;gP^HxL33ysc<_Wt m;{jhb[)0sNpƬdPNM- EUQ|rQ(c!,y*v̩ȩȩmU35ˀF<0D9ĝBh|HEj5šBVDEP<3B*""Ldcd GT #9kQ#o0 *&dъeK xsZ!__VDqRM?o޵+VڝP3HdȨ*n;V&׏x=Z]-" Mf8fy#&HUWnZ;>ޭ @\U7Ǻ!"Ml$752̵`M ͣ?{oYӯr?E,l`_=c,&cCS5hFo(V_};^~Z|#Qah<+CˆIk_9a{h\Y/[~E`adבzWٝ#/4xI;/U%Z$ȳsJꨞY>aU -5 ̓Z2|yEu2ר|pqOyɋ?)|@ Vq[3f 1Q37{LOAR2QMF B>$׊ED@e-Uei]e2w3eJ>j"}`>!&DUS[b-@ yADQAnilP[g c Qg!FDU}%( y1ۡET vxٺUU~=%kUbN??9:u q~ZZ9OUVvǠ9{HOnofzpe[WVo8#ۜO7DPBl"J(F]Ԩwֻkg`N?'n{?+QDCQvq7NQIFuHhqZuyCOH[6L`2ިYUطYT_^Oa7_֚T_99Ӄ'n^ڞSܙÖΗp8N"cg؎sN΁?39~?tf^Cի~gNGsj p @1)qX]CUAL |ȶ7" , }YKZomBH, (yJgRIK|u-~3]b1~ &oyCDBETD@R@Rû|Ã`;=m+-T+NNO!ZkTB`S iv~8!&1$rE(m< Rҡ$H9XbLVuSUDLJ6r:J)!0  3d2gH)d '!3U͝NLCHǞo\H27:w =8jTREP`/AF#AE}ݵz~d"ː!L=D \p(Xd8oU<!k}?<`oo__|ӓwܺy]>}M RUJ*xflo{_|57g4GpYn Q5%K{"08(#ELh_TaF/F$ȋ+uč`ه8ax 6isͲI|cES^ܼdžsϺ5Oy@ccytRR ,O0&<^|\J?ZHZc0bȷaD(7=.6tsG*1 Hfa 68rED2"wE<:QUN\Z-Y0hq~ï<{~xxtzrzrCDcm^VN8DI<ؐD!Z"-p7Q!" Zh $@2h%x00DUYD0Vx/Y}h-r\Q2gGK n\]igpTUM91웆c M|'wBr5p8<6녁07:MV7XcZ:x|mk[p؀յNQ * "Id3*UZi7׳{G?~ 7Q$ 1XWT+zjkUSKΉcW1`j(*B8bA8C0DTu茱ĀV:vktdZp&DYƨ3\!L@{vW TM3~]~ `6VG/i|\Zk,Zl_7bjJMhJۛvѓv=C<1@!ck3k3cTFk2kWϊRG=y08t6o#T˝fީ"r`,@"" ?w{_~?9P@AU*lFB8#esE!Jȿ1er16-86%?TECCIZ"N&5Ԩ*VY™*7e(9i+9fO?'(z; !e :>x] IDAT92hT2SUB z̍%J= ̍14*h@EsRG1b:qi8Zv E;ѐ!0־k[@8GUO#dTY#+.MT '_?|lkWXvɠwgOtn+[뛫Æuˌ9}h꺎5̢5LD}tnon<}T&c# ɰ YQXT y1֍C!,% eqa]/?}ztx} a͐c (*Щ+'ӟ|m@EGݸv7^y+X""(c ƕon_t{,7>("D$(FDp9E^9 T,+Kd 2aZa xr3–3/*3bz,u\)-|9}qc }_D}b':^Լ+Q9)ta3Ž35,Wd& RRuŕK Cơɀ" @și^CD:OEFT8(&O6BCY#,EeVd$`LaMP2ci}ѼWn߄"UChԘY_[kz.![kWM}^=2ʐ HQE!rY*s%²H!`=ҵ'AVf\VEUEq$S֬ooykR`f: @uK z}x^ۛ3&Ϩ>a׷<|7hh4 z^_sђX1$ȱ'i䇡"1FDM•-k wnM26˫v@ jf*bYUeڌ\9Ff~}]:FhonG5dL>5n/b* s?tVeӪ ZVγ;9BHFOO#k^•k7'ǖ^wppp8 D+_} ,r9M@^XK>]!01w-kys/фXRb.ф/9{sɖ[m/@oҙrIs ,<3Ό L y;dd2̵jnԇ AbA03@F7ʌ0Jv1UL)Ȟ"bb1}"D 5.]QdYeQ`dVH? ooE&h4(" lmoo_r|ڭO{z2:|SGplPE(ըDb 4ynViV2o_{z_x8o_֮|N:klѮn& v3Ek;2zOzphMkM%vjMUmnUOƫ<>fbe Q 9eSaH8`CH<FÄ#7H,n3`rT%:WxfuD2"R99֧4u;: H15G@$,9 u 'A~ƻO"ٟ}o)JۿO'=#5CƐ 'kPݸy3뎏N?ȑڭ??'X9k@^⟬vp-S>bYq .2tvΑ3!ճx٥9.=) X\w0/m\/'t6?CBL.R@6b L-DV.| J` d8BAe G1U qFq!)sJ2vd>Иf$4S&3C$֐5͝('I3MIqDcQ4(?|ݣ͍Ͽ*'0(KQ?+~ݽJp=| ĒZKԀXDvQ̲vXYmZeUeF2:k׷]_+o2UqΙ<ߺ}ۮ*o%gP%y$rlѰ}޽~io_v=~|Ca((M Fu/T<~wd?]Vz DIy8Np,Q5d`q]ׯlo#I6e7on|`_8XڝU%6˛&jUf} @3wz sc`T1%4Eڋ&ہ^MH|]ckCi]I:HhLn};_2Ȓkג,YY[[]]M!oIwӏ??;w?Ô"T9Dߌ+3GB8k.6d`2SOrl9f)O0]p -,.;,޲;>[rhggKNt>=+y>;r`%"(|r_Z%(L #Nk >VӍ xd W(d5AҪjԑცTeCF0ʌԤ!Dc9!tƘu( A%D!*{h{VUo _xITYi&#K$O`͕k׷6)m ƍ,/_?p]foaD~D.Δ3ŸgxEodgQ‹xOOY̎2J sƹ e`̛K^ ,x9xtԮ|<_p?jhcJf²zب` f9ǔD% $է MBƤikl[\603(A@",U5Y\Ez,1H$cDE@W}wz\N-嫕XV) (!c3#LSPT*ܾy{ksR $ MU@Eɚ+~?G;Y˨̿TQ _=Dq1 " ATcZUADcwχ/;EiDy2./Vp2({!D9(Wu!K,;cƴ2O;Sz2£/_H-/& p9jrM+:d^mCYʁKȒ:܀3 YВ=8 ȑD@E$+yLժ:rmKk (2hd8}?cj yf38Aͭ~aw`w랞vOOVW:+FpEn0䅋e$#Q4@$4 0UQ0h988Eˆyl Dw7v믽AU*jv\GϟDgX E%ƔVekEDz.WZ|aK2(1۔О͑YRhO_F;[N7MYq3./2SO/f6ksA MjKpG,f=ݩȤ~R.s<[yyf8cD"U.?TN  sGvlQbkLb,{7}̓'9n,sr8i W5"3SL>u_HoGT2 lGyiA ͑O?ۻ;Owvl~1@Df鐴UE1 5EQ3YDjcfe ]B3Y爈'*`S"C Qhf iDDC@nM)8!4Œ)|d u[jFcىW!`^ٺLL6B6ժv6bS0y=<gFBF!fb U].yKvep~z3ͳۑϲKaƃ`4$Kn@8`l:M}Ef쭭n,|{{;XSBzWU$1D3g!rfR+|6b+}hɮ.T5(R5 =1!a㹒\Y`w2D$^4z9Ѭ>S=o{w]JcecCr6c SKIUo/|͑F/5?Kӏ=ql}lkwp:*9b8~ V6,Ak25YY ~6^R6Xwy</\'H g*+3A &[Uun8~m@~നG[ @hjWX][ ƠjlMz)؀RS2*dȐfTTD"17Y Ix  )@sI #"N U<FǢ*f>ECICWH`H++7n}&tW֍kF j`ow@h2GӪ DcvRUuZVa6yZ1Ct<{ LU9*TD Y$<#iλ/߼IFu]MSSCݝnF̭"ou{\SFDSQ n߾#4Uzu9"Zft2ME8BTRF&0eHv._e"xceZ̧@@:X#nG@Caͯ~h'w>1Ulj y : <31҅ \8oͱ_lٜBph3{G-Â0B,؜%!:n"k2$bxW~I9 mYj@@"3EYTD %jP1FmK=1(1ޙG&rҠ q f"F(`,bB&/53"8DRYLA<ddVXEE 8ݿcL*>7^]][jd6 yz`_:jJCnw-_X(a4&g2;3>VNg2(:ǎ] (T3}:HDټc򬋘jh`j)'2* jYVD ~[FmnM)"3BCSz6p;[]_/rg>H4+"jj=XUٴ CU/~QWue< ~ocs]:}{[m$ >~7 :Z%HSZNޯ8˲D0v5)RȽ(QU03Gr|B={>={rZ<۹}9?Ss.a+p ƭv'Z>gUgEHs<ː1G% :93Bq^,Vۍpj4-OәI]"1Ou%t#""gJu`n8ngg_3jjNk|aj LUf̬Vd:\mtuUy߶kׯ\sxM'3Xk=`(fhg) d'TEH5Rߐݹid?/y-5v &SmO&^}M &YUuUJb~ƣuY4V!ND`uY9c40@%B1EݑίuE=MydOXE9"Fdb8)E9ftSn9>š!s:)1CJ>'@Q3GQB2'3ۭN[*k bMh8گ  xHpi2።vaq}Lea,˼DXlq LAĜ,Ce*ޝV;[GY5Ʈٓhe[׮G{aVkДյtl 9@ݺzJ}cx E ǣE T1Zg}>H?p@eSȉ,BШ{hg`uͭ'޽Dky;fb {3,gՓof[v]e2U~h>"ƪ (%A(qY "@8#OsgZ:cDteZYVd_]:VFk>rRHM,Ϫ; >9wz;Ӝz"ɟ zwCHQ/:q c&(2<=|U& %eEEKO <0~p[2I5K"3RԔz[v/,7>-'j(ABY0UqjeQ"B4@(Vi#cJ92U8>G9JˑY{{ƴ9| ɑ 2NK_ǟ6N-q*نmE1_DrΡxJ/4j<S`C"X"|6n<|EJM)B5#"LC!Pf C`P)qJ1E1X'tDh` I L"  ܇ $JBUYU{<{͍˛[^^de*(BM˼we]ʗ٣+&32ȎGss&C\rJ;8_#pqs-ǿJ8Ab7_xԎ՜t LBdYN>؆V4si^ (.?z:쓚$hhi>7m(\ , FR&jg pI!`*j<;Gy^ؘ$A⾀$hyQ+M9Mc@tw{極ׯ|j#ʘ4*h҈D8 <ǮI,+'._QY~I͛'O9 2bBt\_/g"31,[]o3_y7^Y Z嵗Xol߿sӃi #H:umm9k\h8pkx2*Su1Q 1lIT}xvjFU#2!AW{RIwy9h6- _~p0w3A`eY޺x<\onYY0J-RG B d>uTgsYMb4$+ HU\,.\l9*Cr!k/ڷqMȳݟmY]Uper<=fKP2TUDvyzӞME2\^,ZJqEVR6ejeVfdgU3o??hWW`2oV|fr**f"5aTfa&UQ0 b4SS 1Vz6}=5T{,c:3S)I'v;Ը/7GΒ YWqG1}>Wxv e>J.~0BH;(rjg1xth,U5B<+Lqkmi2KOX$a0G:)4KG@Cט4BB24MgP4h4&1!j%@bʿxis=&ƜF+mf6bﳢwZ7_vփGwvd2~X_rkW/]:DF4̺UpTARnx^8ŹlEw3֝& y!{%UN+UʠyeYj{˗_~w^֯$u'λ~mmm^&dSvoO䓏?qW]y뭷n|R=};Ǭ\u|l\E{: čkE5 QU?C]1 h1:RIh@$)XTTkzH̲<۹IUVW+\YWސ1\V֎&zf:^^23Sk悹r:[W]޿C>K?hJSo]j FRDx3$}?8&U.>,jh/_dSv9\rǏ Ϳi :d8^mRߠSRyh' )/%G58u.9O5/#l[y‚19:06A&3A|;P=iR%ġf U,ےG%556A EMhJmiMIp X:R@EUPUSJͭA(`=qR[\LԎKOr :Jk[l}c{ܾΣ{v^}kol:DU$x:θ%8Ss[ g*t4??߇rEբD "U2զM4ReW^~/غ;H@AZ^ܺz]D_גyt?~YQ)}x箻zV泭+W>hHgp8Z&9rh@HdLA҃梜TH+zv֋Z{m3=&I4#,wk9Y^{v>Mgowݗ^؄"h卍?=mOCE1/]6T_ !c&*0VgNjShʆHL &1^&0:F%C[W^> #sޔxc4E-O,\%g)t9/i9wЖW#Q9,/)8m.9w3Ԥ(F=3l3xFeP23- j5&ږҡ4)F6.xbF")I! IDAT $'XSQ0LuцHa$XHEbERRd41D~h0fђT*Y2 4y#*adkVю+=[ &=겲kׯV{@DQBU nwosv%uyC_N9ko߾#BJLDQ*Fe-0֤I 1+:d:1@Hh0irZVZjO\9H a2jTA@%1ǚ"#Fޕ*} 9y9ofӔ`%ϲ"oZ0APW.xsʷCreU0T d믴Zҥ]4O-Ow ȱ7$vD-e^V]tT %&TP)$Ljq4Ь9שdqTΞ13~\3[8}_ylaܳ=Dσ6w{nbtsg^:Fkb2rq+k0<8>(EDC!H- 2@<;w#r[+xd%3ԺujÝ{+>brCd[o{w^X[]tFHLhjn寽͝'{¹,X &V6F*j @'@@STi0XApy,-uw4/$bGUĦ[Յ9f=8snQ@L LV8̼zS(6 $%|jVH\@cA[88r9h2eKU/iHD`ڤ&i&MiTu. M-&H2 1b"1)0 8Ib;&!Hda8Mo[2•wᯫ f@DL$e"֍?K]yfqomM+#k48u}eO?۟HjYVcFT$\YUUARGBUF_`ۇ/*Tӑtz#3;tISGȎ 97 2F Ԧ,yYҜGd@9 EcsG f9`W ͎D&8C"/,Z!3 ,)6+羯"1J EBʔ)c"` vq#*DWd@̸|YK#4/)DacIXnroG_}k/7V(ˋ,oJ.6J:,u|ޞe;h[I5 `Δ6}juzM^ۺvfh, PgSWwntCQٸe`I-(`0%GDC[:h `)B8` IɶU F~M- 5n$ Tm|3$,ESt*XZb/^4 AB!$36܆Dڸ2:_ 8G1eN3'7=9&Mq7M7 U@E I3|ial(˩;@MhzD[.XBՃ[[~y9:TCUj]*gBLod}/]I6gD*Ud4 طpmsw"CI RyڵU]⠜Ԍ˽n3M⬚>+.o}߽rc4GCޝ=7'v+/]GO?|(ĺ$ht0s"^;٬:VǠ*Ҵyv`@Q=.onu;g9vgff&$d$f"LW淟g{΁ f1+E&ƯzQ~lnm^Nܩs;ߜlցdvE>ݛNlY|?ڒssٹ`o봥T]<ɑݖ?)5R-&`2֌{GG& &FLᜰ43%z.zCJ3дOcM'k )vI4 C4GXSRPEUMD³,WEcb!(uH !9 HqD_WLkng.c@C*Rd%|գ`TV%>Q1UT(&ԯHD {-4O)5 fѨ b&qEѣxb=sJ_]~Ϫz$2 qDse {-/8wz@";T$֥i@05vy4"ӱ;_]Yliڝn5*jmmnHd< ooFe`<>ٰd76v<ͻ~wm?5h8Tg`T4ĪkU05Q42f F}5 ~믽tʲ4_$Vu-1hoFDCDu bZ jh1?R9} og"c7gQq=2?cO&ܘ3$;Kk?_`P: 蜴V&ݯ39'X=WXBhfx:-˗7&Š`DD9,Dw-\Y+/0@`nKvisx.~+n6,CSsz}:eV]i5lRյH\[]{.λo/]r{/4koo[ixd"RK{p7C}u(i 1FqdSXPץEy@\1C`v3Q%89t{?A*Tuu]fQTu9޽4(:|vZ)#͚%Is1_"2k;K=3=`4v% Qou$J {lef<"짧3€:U_ڄD!b>@tA)77W',_gzfZPk'L6!ѳ22a>OןzPD: BH &w~F6d4loŬX,H3FvLGG5djl5^jSJǏDh8y7s Fƃzbׯ_ƃz2  =˰!_~bdU*b1ﺥ&Rhbv*A־q/3@f ̡[sL*PČۏGnHMǃxPGPTf B-(าp}ԋ mX;mtNvVS7 'XOOXC?AW.;Osŵ$b=[  ME*p$ ]1(X\rCUiODNFQ97U`?Z>iDPvэ0,;OMmf{흝kׯk !Z*EX]|0 {m#7! #=xя~i`=l DS m: AeD BEB4`T0@xOk;w41'[p?t8<:Jҙu AŃDb5CZԳk\n|yz.hƥMvzK>kŞt9h9縼.R96V&\Q,]W"D@0Ā1#P@<,%'| &.dQjZ4oKU7Vm-KWG34FT.af`iV2K"R"SD<:aDщHt)Iۥfv4.-VEU$T\PkFUb *hXZ^@פE UsyDne07ۨr( &e׵bhJ5sK4IuUig12O.b![޻ᄈ5ٝz_/t~d*!d!z㵴'KZjM30t2up2ܽGl@3]ڪnyܦ$/^``slI:ϣ+@ @5VCtC!_1"4/Cӏ>`ooxB߿|hzx4@cL%%`T?~Ϊ795B7Q Ͼ}rMLx-= Sf=|U[rj^Ϣ@cwM0r@X Pw{S+ &+v`} 9oCא!z}C=e\jѕcͣb/V"هRN20կQ.Wܰ$5IgKƢ)ivmk۴\6e6b4rlVRgBbZjkّaE&MJӬoIå R']RI&d}yR_#D-H0(IГykaצfӣѰ@$k!f3۱hVjP= ݽ=} NRFNE-;SiREtDD8U5f8Kk>zH;vX72ǣĄxSf01]u8Ԑ PfY4mz4um`j*PUh4Id~2!tUC~wo+C?z?_ߎ泦kMY`b@w"`j;/oɟpW|ӳp> .}S6+.zW'.}GZ{6LOd*n9Re/B$  ؀@=UfDQ+[e쓀؛@ʮT?[pQ+z=3uSsaNE24<ۛEm2 lXٮK]+Mvmjinle6IVh4JDY[tyx8&Or3g4G\7> H9IfK&0F$4*`S+Ogsi-3g6ESA=S/Qq>5D*@\x?iT;q#Cֻcc}\qniW0 K#Q])Q L=Iu2E.T,uI$)YJL%[gLD.Mjۮm2ucl].Ų]" "RJ%I-]Ȉ$h<J &V8jjZd]ΰj(#qby !ڲi#S_p>Fh'dsg28>:X6uh/`z4Zd5-Ie㣣]f٥Z8NSĄLȃjX6]1zyE"H swʗ/}rFH0%*XL cIL"߻uxocNU,1qKp+ppe l _}c:庛 3OKTw4rƻY8 IVmg6܇}~1>F.#Xj1Rba 5_'Ų(z- MߏP&$ºqVXfuD`$yZżVN%ZnPWVU11KbTޣaB ԬRlݶvmSӤKm.˶i=af]8F*bTcP.ig Gު^ CvzN(0]]T@@1*]][""H~E".jm:bĀDDD+whx]Q{UGG}a5Ѣݹqmf&*HMe'IEw8ںu{F᷾[@ɇo? NNx$%Ca5PDOK h8m1fٴ])&r~ $00q>~"v[O~>zwmgfLUȀl!t;oW: Ao\>6?\joi[yr}᫚+ά'vO]H=YζOf:ݹfup11B.W]'[zc"ueL2"&LU@& 29TBa-i5ro |`iTX@7Ukc^_u2lfEWDWUϭ4ۈ@ j&f "]ڔڔDKRڶmimUUס3mԶ!0#]״d6[N&z"hD-lP,jD=2&*YDD%{F۶P-xڶMAso23#BRkڃh8;a'_`(ip}wq5d`iڈ62صAp1k'a@i= f7n]Ǧ1~kޙ*p4=lƍ=>D%22GMt05HB$.[83 "R|0#0/=}6cri vߝ0ux_צs%.VN9q;O\Щљ r+cTIPI8/sh\Niӕ6f9$y~DA`ݟjQpO3ŤTZ3Pk"W7=#oe;+DoYw3ce(\Q9猺O-b_yI4 Ժ$92V.˚1W1 pXEF@fIJ@Ȉhh8[0.;b !F@DD@do^RM*I@$YW$j_%-!ƦtiI'%!5EH٥K$Ãí`"YCi W {^~ RRLZ!p7v[bDe0mwpxDh(P$BdfTbF_M`/6~~8 (8 0)!p=ˌ/~n03"1Y 8\xCmb`MLY'tN{Ø:x#nq>m5$`7\랔 o#CdV \Oy芩@s쫩()sU  1A]AUQGMcu e7.Ga=B`bfPBFR8 %5Д1&^| bjBt0'-pOIeDFlRڮ5-W:9z+'^0Wĝ7o sM&voPoַFAדAƍ,V4T.ka5~sw?R[ªѻ;=.(b ]d1T"sH]~okqɏGG~FH:`hɭ[7+_~;wx/z_ytL #G"$uK{ؑǫ?S>ESFںI<BN>7=VZ9^?j˺vŧ2A>4ĉ]}G@4#reI`,,1ARWD&(Zhʮ 9cI$*2b` ,t8{F &!gkS)S*7dS{tꂑkOhӔtZ/H]Y<#Q2#iH|-Zgf EcPC=2NƃA&a״Kil@ӤM˶@D$ ,Fo݀1&8LQdN% Hض]צ}Đ;`u:(#²iQ+䄼S++ڬu=| Fwo\oƟn_Dh;[7;%M50TՓ:v$kG/?fo}ww?1o <|xz8.YJYLL@M-AE 6UBE"b90mc{1x࣏~"諪SjMn߹7K/|mww8ցY!HUoؙ١T wO^vwb=2=hx62]BpοA!_h8|r~3?:S&//Ww|&+ mpf 7 !0 !#j]lV7=j<Z6 "LDu by( $Q?=ͰJ@A?#7dUNU:Ѳg͓#7S]hdx$ںҗ8j#&Eش盺Uĥ<٩J,c(dTyN : ]j8>~뺮Mt~Us9D"F3%"$5, l).&q.U"瘒KDBjm9_.t2#uIP{'%kMLTTidkgg7yβ>tDA* zk]~3oW!L!l#QH@q=d8T-F,`4 n^?|Yw[\$Y%P8]MI#T j5Gz5sk]xLE&ܹ}'n4;xw].]ܺu_7hML !2@d{?E t{/8;8zr훷n!ʯ~ن[>.#OΓQeN4`dTO!>G36^K.&O.XQ)NOU"5K3!3`ޤ1c Bvk9jEhs"16~CLYX@(FAفPL̒Ykҡ]yD3C,@ĜE~g'`SFi߭xx,:WT{8%IUMFg"ǰ(Sc6-DDf3:L  ZHAG`T)'"`J"TS E< :Dd"]Ԥߵ%ڴb@ @!Dp(|6;<<G1ןK+.<`^$ 8Us<@0oLnHnD@z4ڪ8|;nsfɔaf`ptF f\AWd4{M&q^z"Dㇿ/?x}S>~8H$'a+$R9y`2wjw<_;_y/20 oݽ `@n^_}W_`\˟~FO5>AYl|cswyUvO]s_7xr7)=W8YUR^xhP,cRp3.‚ $BaB-J@U̘Ca0ٶbmx@F2&8c? f,@L}o i\.xL[dhNVPmdJ-<R˜(15XXL$(3!:I)F`Sx8:<2T!VL(Hd jd]7o$u53u,@2lt{|οChn4YP bׄ)bRf&D4;6זohf,0 wI#]j\dgk /Eă0ufa0 (j4W;;c2h!/}u!)8"Ց_S?]v]Pmntv?w/Q_!1{!tΝ۷o$I]]!Pc02U Y2Ol_w&P\#ح%o1έ7nܛ/=ɣzS̥r(pn3[_Ɗ{=Uq ?ҜsAIoXw[.}{vOPy{!yL`̮ 8M|ĭɐg9U#d>4iP␁ #"–%8BP4S0B0MP3ZGA]ۘ%\y@ʵhmXL+BC8.Zc sqnp֟5 ]hJijjQ4geBcC Xd2"4Uت4`F{OGjs_Af 203U׶wgF[ Bw|W~[{ukjt/}[$ؽ;ܺ˯߻G|t}wܶV?.wxcpPׄHZ0C6X6\Ӎbbp6Jlnz.d+NWY>E'xO]:Ϙ sp|П)Pxr}H9 H[UE2* P3#dn %K!":s6 *I}#h 9BT2@[$}Bieeh LT) (jkQ ,V$"tl2ڗ'.-㌗Nf0[i N_5%2^{9]|:##T]Pd`(bN(VUUfuԚCqMt8rɿ,vg;m< ^pmk>nF&u֓Q7klVM ?L<І QO3Z&Y"16m9~{ =/c\4 xVY5oc CӌI|9~U7߹0 ־~=ISvN/nA)q++A Z40vby{x*\},EdfK[77d~t%Dk|qФڇ5V !Q(d&%QTkNȍ$eE/llڝ F!5L ufI:U^I"bm.t>;/vX=>jc Mfi/k4]q9QUGڶ.b8<:٩b߬+{+ZyDlip4dBe[%n m|2D6gS_x{1(Ř#{ipp,7@4BĈY6WEẕfIݫ[L^=Y{ZϨ\=Oq3>em}h2C07N8`IUME WԒũkoMs?D.żQHe+gD3_^WB@ YR%F`&IL3-"rpÈUVDӋ}tI"",fM]؛)R>fC@L|Ay4M*h&@0 @Dv a0H!0#@H Ɠdk|tpG{dg]pdZʔUp٦VLJKb9_,gl-ŲΧ,FS'R0㉊k6%u3HDK)UG;|Ǜ*+O_n.$^CSDD@jÝf6ڑdy6m޹+/1h,yȶ[Y  3KOfUtzf,b.'O=y6O4Ż/OgB&cBb3g`LȐZ0 RG~]0Q%Vjb꒏9 U< ?::ىΗRnO9g;s)e&Ka?c|+F@mBg\A0Na]3r6~v_@PvG%ђ+*n ˻ XP!=O*[m j]/fb7fgq:69d3U#~q0& 9v1qN!R*oHσrIOf뜉#e(+Ѩ3Kh``+(Rk';{W ]aVt2 "*WB%"63umK\ B2 1)i7&׾l9^3zo I$i+* _ATfTsc zf$#DnxԦcрDL$Y״*R* Hv gr];_,Mn٥8B1p# ,̪2Jn6-j{{ yO|8޺b?W.x<_k'1ϼ'ԥhpf18}奶IqakoۦW \2]}mw~'?τK2,mJ'8k$y ϐ~nyW'^bRLspEyQὸ *2RԤ$I"kiRRl$uk1Xz%2PQ}%Eu:\:w$lv =EDcd"w10cvb .yQ0DZ cĕ!";XE{cmz<<:$~%V dr∬]QH JB`&f!HD6(c0P@paRĄt2ʀ+B1`{kp {Ã?yq/\ MD"F*XUU 1ƪUDDt r]e?xoȌCV j jۖWXv]\.'1e~ǥ =~ru#ˉ/ZR{?:,\2AyY֏Z\w ~ _!|l2k/<\90.61 3ǯ!GnX65ˣO~㫞g p{%.~DMN{x*fWw@!2mS2 XGd8 j9'Ph L=oxOҀ:ÊR` ,~ɳ..ucb%L7qarA0Ǫ31zfr`/̎TBUUj9)5nd^u7N+CBΩvkx KB|zh Ƒzi6A,PzP/f4# `P &ķn;7ڽl6 QC d!VcX!FJ>?Bd"t/_ÇDDFFD,oM``i/*'l}=> r/N6͝pr-%6mk'q?GͯWj #"z: mrJN\M28vh׿< ~fSpR/0rB|.cGc`D¤BXQm-zH2Sf0 sD  b^gl1 5 x<SOl6lhb^֌=W-2U5q95S鬍E?,L觷V10qTN"e)1X "eP/j(%Ө/*˙,S@{"g8ci90TQI)B^oPCUC ׈ T [MӲ1An!w$۶gV wdU'j͆K͋'pA%jO;\#bNDe H̆z)-*ٿ!&32֩5N?ϲ|..xt#}}熼8X>YDXLǤb#1fI+GZ,kt @Y^SC_} ^:TWDL0$S-\_25kHv9#z{fzǓj4ݕ3IwKUFÝan33Vy Jn7deFȿ8J2Ѽdt:Ze-UZP2' 'ь1.ōben6<'C[xhn\`\JK&*N[3CD1Fk|xe5"vG#vE4IJA'/.t됋~ ,c,fQ^ַmnv֎6r.oA{D^>,jYDA$ wt([@qjƎg"tY=/FH~˒ @(QŽc슠6CϷL["i>}n}d}֖݅NYRt("Ba!(1*Ϭzk׿OdFʽZ+fP5]_`t{ѭ i`nD{<ڱu>Azd5{Rl@٭Ѧ(o sc~Cl,Nқ߆w1s_Û%}c{aCmX_.]VK/퉯e8N%@x%#0Dx+-l,?`?T< @Qڤ,I^IE]F趒?##)RnJ{Ey\.Ǻ.&AXiKäDst 8C7hK $ 44NK`<7 \Y&ge*nB6C`ՙ$]2ÖKz膛EffaE]lrU#]6Nw-as,b. V]M0L׿o9I@F]?~ӧI];0,ϯ܋!:uw_|Xb u- zvF^يPp@fmC:@ ]n7Wr"awGtի?0F"o:} 8AoͳJkwf# ueAYR F=QI07ĆҐ2 @2b"`I;(m XQp)&9Jd0{e啣X%7_*⥸#@WˀTT/$NJpe0!sngU첬B IDAT}='b*hyq8~X.u^!]u~Ô aUnPϢ8+ڜ,Ҩf >\6šY"7V-tliz>`H6%ȕ1FkmabKAeKNчKc.Y6cuw5Cn"e1S~|x^LhDǏpXևvq8ΎpO!鋍w[Ny"b^swX|_ 5s285,PTG1cӁcrYdIo>׶Ry}o{|o_cynJowl#8yp'r/‡B(Qϐ/Ѐ*r͙@ _cmGO[yUǎZTʮ#F& &Ifn/wvL@ ->0"E˴fOOA#hr%:eN0YF\}":@At TهbJZN҄%Aӡ[`6:zĺ[EJQERǏϗ#p&rw؎HL8]ݳ褦kT(`&OE!xt̠C:WD-`U[D;z/.r]xo|5-OH.ӂub@` H<)rdFY(-2AWQA"O8P )?-!yTbUC>Gs=1y(4=/[`Y>T4R͖eY/ D:PD1Ik8ae8DrJ61w,G T5ua7DaL28MܬvL䠈 c)/K1J H;c2ZpF!XP&!c.7kEkaPkVaa.\ܷ6Gn Kct/АƧϟ?~/uRwLLks]smF |8)}U,pD*Q`B@@7J*2J4a%ifabaLQ. 0]d Kúغp]".NpWU|D HKGP1&CT9똥HFw+ӯAc鰑2\g4l$L)5[d8FFѤZzZ]mmYZ`)92hm]!EyKkqT1Q~V:+ S۴bda'lx-h(yxЍ8ڕ-7kK߰d10+KRCkpkƐ>H#JS[\3.VLŒ&*cW1l qr#l~YKrЂ6R$=?o 0}H3-ԏg6zBoNftD 3fOP? sא AcNzrb5u ȹ߷Ϳ`{{sNkj0ȏ@?/ڭ(P[AY o-Z^V6#q4f(2eڕy 6.d,axh,37~؁RHkFcV-O szdhRnF6[,‘. E[ߕ"i=F8` =i:gFhZyIҐ!f0l8,jє!ڗMY03x_<*ܫd-KLh֒$§Il2@H"Y걻fR& `JBQi-RŒGHf@3f2u}}ˤ9yNc_޺M4f̵apJ֬MRKGR[  `2]8(;TqU U?3mw>c{+Px<gR9xsr!~Џ-7N^j,ӑIPnbmj,Ӂ"Ҁ٭'n0\Wv)rx0^:Y2i %ci/Sq)3ܤ⨓Y! T){LCH^ڍ-FiZA4,OigMwwmiNIޚ=)S{fͪPx):N}x Ni &Pmʧ*|ZJ'ő>FlܭN&Vf?裓Ky@ DlITw4fmQ$pi[UHBV.%(w.TX (9Tb8ԇo}\y&Ӗ>h3YF<~>eIzk{Su*{s:cFoHDN3f(h gFźrv,IC59Rn1QƸ6gKήgKwh)|< -LvCPwV~K'7]5<]fX/X׬&qclR3>>-v )hv1_N}ϐF6= IL*3ZOS~Hcxd`#) a򯹕!`^1Hjۇ `6/֥] :kDuiz$Zqw- e\0D* +#n#}bK!d9T$ˁMfƓ,IfJ+ 2c@ Jq@y7jj6JZ<L GkK8Z@{P[ZFN-8L PF.,!3P@yvmimiL6[{z.mI* l^sҾ5#TV&t3dAW`І$ +g)шP<"1!Rv-KNY3}l>D((q[&87=wJ{TG~T7'@ߟq?}u?f߻4/F`QMK/p]тN੒I9&yBvaa 9EU]E}:8#$ M_et>4\Ch>D?^0|TmV3we,4OIfhqAR?oYuYim~L c!Ee]rQ#'R(5[w$EVS}P: n+qTiq^Z;FjQdѠ f҆{?rbuY*ΌKY%剤mŠԡ Ow߶r>&$z`(}D׭o688~e5Ơ}ӧϿ|~ p=|uP3k$rUA|FZXqk4&T"j@dғܴ, V񞜰@X(͋`Ktjd|>pUaޑ;[4om2~ oL~oyqIE O"# 7rJ 0@8S͑r/_9\G65:XizO%u{ n3իyXGe@P)Ȍ{6ڲ6̖e\.˲XDF¤ c3kh]73ϬGDGX0ES}*y]mvOE$?Crl 0u/F޻j"nBzl&,P4;*fGٶ,l]Wf4IC*(UV@-іZ0h鈁[*wbFX +uֶ$+h [,n+%21>b)8 !)lODfS/od  \)x =vTD4J /|='q$g}  W+dNgW}їu |nwL/e{Fb*ht^˘DIsAed]=!"6ժ8,  M$قH Ol T |Y/Ʉq#3f*@20G8GH=}XcB#}1z}SS U] Ǐ۶]5!_^&ouI9ESQ&L[rﮗZs'Z/k an^uBE}s,!w;aƧ  mp*ŎihY$mԘSabDfmIΡ+[ѣiK$L {c}Y:]2%}OXT. X;ufIXJh; 2"A%f/0LӼ){Ab3qM7?5SLySL8~c~1<=Aۦ ֬-\ Ґџ7OJ>OCh< yʋ]%K7zq8l4T]8% &U{3 N2J8ȂEGJr"3Bf#4JX--z j7{LWer>"$Ⱥ{k-[CٞI1egɖ9- $1jW ٫ pET_ /BlخS0Q>|5c}"MOBÃ.925!ؤ>gn'4M 5K:1cxV+E~)|{Z(g렚 >욾d:3!ڡ*aĒ Z(<0fa: R7LK L,ݞqsEuI&ٿ\TN-?z_{ԛva:9H6pY,0C[FF<gi:i~c2`G!uDg4Tz=*%wF{"q䟭-͢G%Rcf*,>wtQ3'%&4FsƐ<@d 1v@xߧFz$ҿ~s |k/X?(FdWBn;gGa4?Dq7 /KS" W믲p:uPD@zQvؠ!!ex@ :,exsn C .#b T| d&;F'TɈ6jH&lF I$0sX-fH"UA<#K"C7h¶meY Žǁd2~T_mN[7=XZUߋk\">_pc4GI06Z~vdOʸ,.@tJȫseH(Lh>^?'dbpq e,zbLܧ' /iQ%#"Q xI_LpX+nG3]߰[1<̈́~Xl:sO4xzR=|Jݾ?DeHUm~Ex

e8Mdf>܂*!.#fGmԻVA3q!L4oL!l 5dfcY36[H!2xV6hHO7'D> l0K[!3]f` ض]GXNUN΄+ Qa/r9Y.Ԍf%Pxc7iԝxx>qw`@kt x{uG,EJk&ʑ?;1o?IiMf֌k ŀz@ q4ITjY콲_)}:oNHK$c(y#9L>F8ʒty~GGn4Ir.LqC Ev{A1Вrx(C=%`kִ,mqWPh(wRa3R: hF3X)ΏY8K;CQȈb" *c -bTk.J&W0Fn,|M5cbfr0˩DYߛ AC5k=,KKZ$UW_ `E(x%i:E5_CvzބX!\73rL`|*礓@I^03KZexH{EY}ɱ& `U1z=./uWm;&"Y:2#1}!ol2hX5!D:xr"+T;ݯ16yOBƳ<;Pxf=*JRZK<2-0B& Tj%5tvM '.d)e}?u' da1fLGXltAU( IDATϟ=6ec1,Mm}cxHwu`5A,X6fQ kk:AVl:hw-4ȷȬa'1!" r3L!Kvc4CSOD;$LŹ$5jHf<-m9dpi8LN1 t dhYǟC֬79ܘ18_Wɗ\lOs*ᵄi6i*&:yW?~5wv6]onp=mD$j]˂:хJu׀;Lo]DQt z';ΕFqԃ nvɮC58$vͱi W"vKI)F0*acK#p4܇-F{f" x?/>u0ֳYK8Ӕi&p<}y_8֛:3&z6m-Qzđ4Y*{<]t%WcF›ҟEˊ`w5TWbD>>4:zmw;_kQm {eXVc'&39MhcԜӀ̣\6Irrx`J) VE20ce8)UFvN`:JI$3Ç>=x2Q%-i%i-迒dmZAAMxDȶX$E}]IRPqW13d 9k*r#,HgtRFilqlI-FlZ5&1uB~gvnT^+r2w{K݌흤6>BѶwb9֥+Ę^be˅KNtq_! jv;ػTG-:ifFF2SHB骑ƎY}( GiiLéHCD{Q׈a tz8-آagrcuA| 1G]peY92z73[ڲ!o&ܑ̦C $cYnƲQbb4+%[*0`?R=r$Jde]>%;ߨD=MifEיf"'UZԫƀq֬:;HS=oX/ vyg|<}/sԺxAQA&Zp`ZަHYx8gl׽R|JQce]*z~3|[z~/T}~a]#h6]c+b~@S^^,܊~̣'^RE?t9ɴj0]^k/?ܩ{;`, #"3#;@ 5GDK\WXl\Ŗ|ֱ.xDc،̊kkFpDi`"~{}apFԤn*ҎiL7kcSP.Cx4} ?2t( ^I dT11l$<ѐ3{>Xk ,Oi@3Yr%/KkQ-jXB7g Ɇ$3`wMϏZ>p׈ݟD#R[AFZɱ|b-ݑ<{5̩ ɿrc{ X<`$^ք}0?|Mb] \ h#:7ayWzuhaW6mrڞ.`+JI57ʼZ@ p#W*ӈlwq0]ԛ{<PKm&{|گ#/pRSk ԅ,#2] #GYHϤ j\ ,+9pY[< JYZ$'%:SRՉDt`Cqii+#^=Eqry\@d)ej_ODs+' #)cIB1,e2092tF`#ib4k,JGlHFRC(M,U *4pKkht܇?D?|CpPYOH[#5jѵeAw8I,m QtM{rALuiciDM4*tK5H0$Z'ݶ!ZdAך5k+ZkجsC0PkKGK`":k V1ܜblW&^3Z&?_].2%fh.-LIQk*-q[~lSYm0F_yR x|ǯˑ4??vPLp4ٖqifM,,m>M FtOOPyᯊ`zhn@3p|ܾ<ث"[!BzoHIb;;t\[r)^&5X?,bMn CڶAXr6("h4{_u:M\ެ>-]ۉK!¤:l< j=/W~gdA p@13?`J ~n`!/UYf@f!"]D ,l$+ׇc2)޻zdRxMe]v~~˅  ߣMbK5wq|mPo+o_t ~o϶F*4p'"^kh hm hAWK{@ԍ_%b@n:b.Þ!dd ",ysj =B)| D\B T_^)KρM3D ӁqHCD8k 70#FGxU#њˉ&!B"nlrŘ,CE{k۝? {Gn=z.,-QZ',jeTe RT\\ t^Uh͈NiUOagKѽ컔 5BT Mòa|4qCNrY#W0~ `z2p> Ǐӿ5r]Nav;ГΕXo2ݿm|O _8?>̮GFV~;sd(G @C \&~_r!jS\YdΖrGbU&zR23f j\(4XqHHۡ"'wo7#ǐXʭ\05,umaݖGgdLϿ/<5"\Kёa5'l) 7Z] s{Qky|BȨ+n&\C.[[ܷ\Moo{׃~j8̸,zak: \iXcӵo.IA =FwBK5Ubʤesȍy?{άpMEy c7a3WpO)e2!꫁߄V3 n.dR<fH^h벬 ޵^R Uz,^.IȍxTֻǀRH.pV_$kRۍAHÓʯˬܚSbZhbAY#bYO$ExgV͢}e,8)'P8kR6ҬYxQvc6,#"W\"CtG8\}]>zZR{Aa0ýwmo[ [Y[ ӕTaAz~#.{R`÷;p~#ۻ.6xxzM|"<= С PFZ:H[1qJJhYঅc=`i|USV\YE':@)pQãfpYƑ>#!ceG*}]C4uixںxilr<=e8ZRz{ˀ]:b I8MË/KR@1Il}4ucYJ%䩀X$Rƾ2cƴ"Z?C}VҖmzX6YGruDJhmx#E䑯CMjmÉ0ܻ?>~o|>oOf륥NRoʶC3NEExp뷶MKwk?^a? hqzfpoFbbhC6i|RrL*lK2}0ػ]yyi(lVN@]'M#ڎ&FC!E0YQ͚UFkFv-kqZ$<_O޻YS_\.$}ö1TZc5:eijRaKL+ۺf_ZN38dXW&8%H5;B52l] P8PQTejLњrY;%0É;Y[#ٖU)4 /5ƒŬE|54[tI&|o.-Kraq˲ZkpӓXv;)}\y/nˇ$b= H牶p#9\/ͽ^}CD쟷c8̸, (99ӭژ!T[κ_0_u a ߴ:z4§ۂ 7 JtQ QH` 4\,袬1 >vOwF |˂u`C¥-!A>"sf acV(✵`b^Hq]<O)* -bjAtf7 K A 址 raO !Q pn KGkmH(8RP!&Ξϑ^ 3YE$i;,ۻ[hMi]@C@#M]/SĖ #ܱmcO׿WKczoe<䕷-sP}mw=zxw8va7Uv~q]rL1vSn exg!b~PVMːn7Ȳ'ώ&ead%hOqt9vtN]BU=(iFTpDz1cD[K=zPrGf"?0Oq<l]/miR^Hjm؛?EHw].O$Ӹ, j HC!69822s&nxh?;M|fX#?JadE3XK%5ҍ fD(2R2&HJ0L2Eݥ+ڷ2+ Of|,9"UFb,&AD%Z y`KBkMe:9`OjGyh#{X%0F[ IjWd҇<FLWhz$&CRzx|s; lh0#`Jj`A/{i_(QݨF)xv?=ӌNfGѬt/$_[ĺ\$]0I&[W4 $l[G٨RO/Znʣ>&<^*ܑ+Z1MxF"QY=}& I &4Db+(ӌuŘ88 C3[֝+l4C1TG?0 -!6aɌ"g39`mas4n|EW|Kޖl[Ul[\77Žu8ɻhJMٵTއӄe;?Q2AVՎ#C"њ-+7 @8t6Ig10b;]5m hp}D iᥳ06Ii0;YFM@59o/"=" eYtJr۶T͖m51zMl΁^c1yyv-b@ E  DYqdLݔ$q8?EUM,8=er:*̖fWK::KY.a?02䱡{vDs0(4uxkN_Z[U>=_U>@9:X1ʂTY"#3iSv~wU׶o=(N(^g k(Gf–7(=ѳJ=i`"+ cflr;;ܭ w۶eYZkc{K i}  Q3.^|Q#YJ'wʣԡ[Jd?ˊH5r| )JG!%rbT5w̆GdDz=PO pXiiILZBa*+LY#Xp4:`E9~= ɤSњlȍР񃙐lf+لe˲Xk^H$YzHf>~ҷ IDAT˺.h1%@(ޝJv׎h][,$lz_T^i?mC'`Zeme88G]N?=7s yæA{|_7Tdޘ+j}z~X *}B@:)nn)cd$ҭY0řс<FL5 & S4m)'f|]>_?}|zz,6em2 7Zw7 /ke;v-ڟm˓mmzE.UHptсqH:9Z|YrN:i٥bZ8tM-7=)-V?W.WpgY<c8%m#ow didiifc)`ػPfttRAe_ެeu0k>" x!Ӆm.KNKc _{CAQo8D'#)ÑOʬR@`y)%"ð%;y!Q򹪍1CO кG`CfBEx09<hѕf|9*nE+eD#2 "aZ+>8zac,{>FLHYs&>jH;^PA<ǼV|Jm,5>[y,S-=U@7Iu=g+h,ū! ,OAbUg?[iJryI$hYy8"-7 ֑6wft6Fnۺ-m_Y?~ # ˯pilX+yZ W?PM}vRe]RY{_7֋LKkݴ?RIm;7|͡-oʌAR9|n:u N Fa9}{հqn]ݐ+R'$p2.T:kpd6VV&d e֭'1[W#$MGhmZ<(J~,N=%Hy^6%<h}y59}9szg{NBZ+Y`߳_Y!stfpbD+՛w0Z3b51q (I2UryM!>ID1\M2nKE˶mOKZtmӶ ~ֽi0.alJuCU!Q:ye롎(m|6ۻ?j|E^=w;yc]l]NRǖ]/k+^BHicx&O"uK0QL{L@7/GD.Tz{}-_#3_zμU%Xܝ40##3#UZ&'i4 . P-mJ4ga޺B3懸!c?A|ThG1so6::XR3:ƀ"w)źhGZ̈:Yt6:d,`.U]j^JxwWUf(L(36{LAs<2ݹsnmoУ<,ȓufOt8%miwr*,uCy_q1 D"bZ ϸuXj6 >‘p.G{jnYWpp~[m㲆aD:>Mu?N.tҺ_\o($VMƫ7ysiy-4&7鐏Lo ZT }' @ ;aj=/ 5K"g"nZ-=;QoC Gj#rcx ޓ&Vi86,6wob$d`4{tl3KV ›lZ8հrZP,uDP$Rܳ$9ͧjX%Sp֘YS fQ1&4,Õ  #Ҽibv;xQ,V".~oӳ ""!fLoB&gn!^NJ32w5+(ޠ'7^5殻%1jF$heװD'-3 SFyx^phםzr;9'56o`JjͰY@M/fw4^a_>|ř?B;[vXOJo,|&ݻ#:|]OA.¥0%@v m􎵾5{p!G2]E '7-P0ܝ4ttI÷rXAЉI+5{TkVUUMmGօ f :13p0sWÆj2*Q9I#\P@2,Y)AG:II1Fqi^ýfh1D'tS3GRRj@ͤbm`xcøt~Pe#⦍!!?V7|,+'DD2NFc^w3(+z&W@;":v&nJnsG0A F(5 xDt>G@kMm" VLn|OqսVU5ULKvce3g|X8䝡ƉC~8𦻸XN_C?w5;s@]X`&wg[E4;{JW;6 L2PAw~xcj6C.}n)~+4R$V9<돷m"9 ܥrz&2%;uT *)T= p3~0LFn08VnEK WQR!T"\Z ف"!8x("%v.4"W4gGw̖f]h ܒ~h#)P#ksG:>-#geW8D@0Ø(XN?_ow 5豹,l|e1`08w?`-HDRJ)mzzvSrT'aDpwVSZk5Xd]m5 68QW|$"y2>d'[џ;ؑ꓍EO hcw*Gfce0Q@ ؎EíѶDVD팎EHw@pjVF,Q|jz$Bef[1ۂn(\?m-Hfj_˿b߹ +e1DŅ07SWx,휽HO.mayPIBiڀTD`U }0A F :b"[ЦV'PRTI=e"Cףf&J/!PiKJLKƘrDpp"oo9 P tzCA{ oχ唣Z>a'x #gh{ l,M L\ =m֫j5" ,>v'rsxh+}L+t|w8Kwpw:_?}JnxI@gۘ۵<.{8 u?cý Ao#;^Ӎb`cn}8Dn^D0 s-`qS~hgJ<[yy_o`fq8e`"tX0HUm V2#gd'' U]ERD Z4P1Zk&qaas D8 "#H}*/ Eexxgr8ݥ: rkXs FbAiYwQpZdݒT"YB6os, BgsUx!!dpȧ ށ[6ٓpAcsZ;Z Qx0pgbf V=p d0Cw5iF@[k?5国}-/{bmop|FKB|r|ހvp߾rQE;{qLc__+R/D:B8*E>M;"APLޛ{j s9ͣFwU(ݒ"n awj4Qnj B`sLU۳ 8Jj Ɍ0"120ID+0N#+o&"ScԔ"fØ@Fbރ'2xY0;&#vwc3Ԉ -/X菤74U7yyĂ" ϓ-&HaFը3GFj,) 9ͭ̄7ܺxPwjjUQ)z.ܧiah^NJGBx&sXI,. K_N}X1fqS _ӧOޗLH{K0?[N}gy#>y;N$~msݸ|"9%n8Nu#p ރW#")!$Ơ)جfq kͭcaEbn I(yԼ2%\EnVFd 4Qa0e)^ML W }A,-JQP) e$ ]E!7NGB^xȼIٞGJ\y71fRx7*7yU#F QC+V1s5dq`RZQd8ZG,s KKARjg!,"B+վO/^u/WP֟lm|p^š磶8}%h3jBq$o›}gZ\"0Gf}jζR->'wki#L ]K`<'d )\A 3e`ZVu>iZYDknh]39 -(qD JyN3LU]|(E lN^pge!w5BAX2#Wn#%ٰ"ePV,KZ*і|*5lbgse9M;Z渐%thX]=eFo;6yJ{Qٜ¿BZEܪ9pd0HPժDDl0uWPnd}]7sYW]!Nk:"sԇ&h Hbe$~v3DxF"  :Vj50 [r,zG mZ? m(}'awޣr!~087COn-{!:-Q{n#8x6́-H=#YhN56:VpZ1C ٙ#p1qK$5ErT]VSG@RBQ,4LVf[ #$NK)fVBH (2#)͍ L Z" wQBOEܓ|w|1; "G"%tz9*N$ {r|)A@!Icln4wGjLfQjFjAՌԼ@{0x$bhG^sM"͛#iQ6w)ެ7gg׵7@irwGw/e(unE'|Bp0bt;p?{'e?*ؔ7~ Aomw̺7%Ǝ:/!ItLfY9h㽮iC"v2oJwv%I f8Rc IDAT$53Pjso]*A6Gٰ^oΦUp] >`\ץ 0̤klaGxۻO=/>Sq},Z=;:pF9{U$/ :Fd;TU!{@X_jC$[6aR!ZS4vfjqrE OHHtNaY<4a5X j)+;re3Iе"&xR Np1A0GTUlsa\HP쀙I ȉ1O< 4  ק-4w z"p'^뮝y֜NLiИEfhI2 >%5dD`;*SVٵꤥ5%|ZWCWp!qY5U#R@CHuceirO"/n/XI&ЭcI_QIjmKrT|nwX.j24ΗYn`XnZP(Q"HH٠!:/m;ܣ)@rjj6R3qPh5qSMMEzEp URUx8U8 T7|1&f-v9@at|͍@`<;1oӵT][W)]! ,dP(pVDXsDZĪZ UW1[@CUM'EPkB OdIEHEId1cqtBz#e9qUu&9 nP}]vf7IaB@'c,%0>+ UKYwRVXLw~k8mokQ?m}XĎ?dG8l {Gl`Gj7h!o0B>6Z T̀Z i-,fNDfԪYX sJRN15sb" !ky$:7:"ԒL!uN(f.E[ZPX3Jª>2nX^f)"E8⠃t֓J0G=1/8>|_dw8i+o`bxy AэX@Y"Jo#G(ޖޅM&{C"fxl4׭]`D=={$o\Q-31f`TDQ?6hvs"3@y`Uc.D`: (׾Y Ex|ADB<HR-/LU# ɘ3"0F XH4q1SKqw<%}jۭ͝LiqULnS  4v& ^ hSUpPZ 0UUE5ݨۧk4 bjHDJC)]P,В&AtBJ dPV9Сy0;^ku0tv2IA#OxjU+ 2& ўzrnchr'{|/Jr.{~O">rCN'c2&Do0)DJf9z#r^}g;1Huz65VJ1g6ho^ 9a-?'[ ]IѱDƑm8&'Ѡ̲=}7Hm ZAEh[YԼV^ dTEjj UhUwVdQi"TU1E#Ci"ZDe E$ R*B9Ic[3V%b+aO3Xh36f]\sxo;8uT^ o٨&}At*HAt7Ϸ?NOyF7q~z// (c15 Fn<0sӕJT aXO?}yO  jK=>cyM\"p85!uAgPB[E6j.L}-A/Zta2x&bV_N,,RR:E檈}6W$QD&%gv/@Oi[BaۏnOl3LƶLVoZ",fwM[K:f{Uu2u t?j`s:Ub$;8}>;wU R"rr:̌X! pCHhˌ-6HڨxZtmqܽ2Nj-&$liSAhxAUtMz[W"#$ýfG{ʿyO2կǺf~d8ER-#,D ZNjReFiv.r=W**볕5Hn索ul՜; n߭7 1Bk&Syg BI> k"s5j{ȕ֚80Uv7ÉpXfF%-3mP͒`(e$=yNXj!~=6K9H3s`J~Œ9KQv8Koq hЄ<H&'rf*CJnFYa'[p5c{qܺ] x'[w60nTxz`Ut* %@QKܚ'$:<;y}P~q{p9{rHs6fl~PEsO9 8l6Ji;MȊV,G@T R,w#"y#ȐzZ ԼQK'{qw'3$L03 eB4rWiY7MސH͈ s"d +YPi2EgD8濽y,~>ŚM--]pQ;>{a96њ tQ,83VQ$Z4MU j^1<ū񙟽yW±@RN.m,8{<(&+pg/^\ߏoG|qaR-ョOȣJww6K#EKĊrXʯfخ׏C=ڝ]ⅺWrf" JƐsPׄv [:)q{prJd0ap&)V( wK|@FY\"T$}6Sj A Zq,^}u(Edp*!A" `9g"cZ<%Lq?@x]W1!!,j蚦W?]ny{w7aYj_9?ߌ~?,b|vx[]^MxK.p•Ak?LDLR@?#`h#)1O~#sM)ޗQNkk5#vGrPU#fSaV~j蓩VU(WSgWUKaPJƈv wfu,WpJhr<GПG ѳwͣ# grI.㏻Reex< S]jEF@qQU@(H1r;B]@0:<ժ``B ̠*\h|F`ےݷO*!a Mً'[`D^]cE)B =XPZv|-&Gt܏>qۛudŵF/y/#^IqY?2KGb:̽VvM̓,W0@\ũ]fzw&WT мr++94NS&bu51S}`.n;Rf ; 5eju2&)7lZbI#zŽANkq;n7҆ÈOtqxsm±ə:^7OϞOO#ϥ~Oe;l7TEd}ub+"ΆG_>¥8Wzqua{O|?|}\o=5؍W?^7?WgS}}S_jMc+B+GeuiɊyM7h[,2LJZRc?)V:kH񫝆ZGNP5Rn Uvi'7 l^EnpF0fqËjJ4 u~G 2RP9;SJ\RoTAv׋ MR< OЍ(vٴ=`uޢDpZ:\qbrXC,fMW xopԌt`Gj`-4IFO:# 0Y"%c ŌY-nΌP>jm xS6LDDЉZCff}33RK!˅9ݺ;KD4,LL!pHYc%ذUP7bUܦIK)j(RiZ$!B)%p0HPKSu_ixR3ES> f2ڇ-%' Q\̙ <ޓ>aΓg@U^5HfFdFDThTȚ̐+s&1EjhQ8"b#6̈X$#F$07 vOQhAqsM S3 ]@I7 !DKXDVSXmMWB?#[xoN77, IDAToo w}Lҭ*O YPڕ+yL?iN<0miR_}¬ /ϟ㇫l<ɗ#ݼ^m <ߊ_ŷzPxyUZV'k}+On^w.pq'~oϧ/ܘJVj b_GǻNbRXX3 bz1^r'&h.CBSw'fv:̍[ٳtX-')*C57]^.((l"LQ~Xn,ʩUb;:*3ZEu^x9\+P ŐJ1jsh{}͝pt`Ŵ[=&9ْz1,am=Z7"R'_L-b4xpa5aΎi'{4? 1A7(tM HLVkiR2Ei$^@hT >Ksj6Y"ZEyrR”3:8\E&fLpԈ3#w\[NLjc~%#_16"rQNie-/P@~던OW^7gUՏOg՟^]=,lv? ?z ~/'(Bru{5MH~\^z(@HRZ2fk=?Gr?bar_,uI㻟ςY_S#8}bϙD):n3rJd {n^o:g!ղWKQH ZTzMvo fqZ5R ʼnǩD #Fpbݤjjew3V"uRMI,1#~*jF03AHT@Q%5N jijb/Hz7`ZŃNڶmտwb W"Fۊ>G6qlKl!]ēZa3̓ru\XBHFdxiiCke$U%83̬)a|̣9`"#2PACa.qS7P"Z:Cd֧: "48SJKv3Th?N @yl[][YNM+hf馔'|VۇY_xrEΙ{MYmaN1˶oY iZSfz|5؟p¿$TN9Ql. ́#&&dqz*2N^ABQ8G|$cTS ,}Ր*VOS,At2)o)X9Ɂ]*dJ$UR*\+Q.RYUbNEh=#jvkjZW5UUbRw7/aÑbp57fMNxB\,bt?t'8H_PB tO/YLLK5Mr忴u[j  0sJ\L)hC-PWrXr0܌OM!C|)Kro1bdMc7N/__'h-LMv#+i1 pbq{zy+kz?3*u/jW;վvqzRg.W0m7_і?^釟?R_m^\__q\]|IkF[lmVj]?HD&BD3 )Babr E1 ge= -MvZ@nX@ SF0fa(6M@h0iZn܉S "AW" @ͷ;HfjɃ5ie:e1C> H+?s"ۢP`(҉I{6tLE΁ S#ƭG"BI EɅ0Ӫ,; BdKjj78z7KB(odj$nմ  SPMps7(&n3ژ m)+4Ձ&03Ȣ;H6qoo\ _[cv}zM㥿zv3| كߗlo>MmZ}{mk~'lYO)Y\zR#vzaj=z|?=:[$$ B(".7Ṉ(!0ؼ؁9줾z//ƼakDHȾ9ñK,7X'ZjG]\@E $M Ӿ56kYbSȹ,(]Sj ܼ%P>v3ԢF2GA\՝ ps!L4D  tibgRV+v),jPDܙag˔=$1ǩ@de9 ܝggbTnMCЩffČ@ (6ļ'9Nf 'G5s5cw6ԥ́~Y^$D枩$0snL!NH%Ԭ f^B(:^:PT ̅PC٨#Rj2h<=ގ W'm"s0Oɞ|˟vK|뷿/^=|^}~yZU'qUTHvj^_f/7ø{WluYN67x.v[ܮӋ.]|}9=\%e6zX/{ƒX?A-FG!@2֒ l_JK""m5~}_4GsGԏ݋1]38x7'fNqv܆/9s/iO3efCʢުuWtW5u~b\PҟuGo$yDlYPH@bj?{t[V 2>ߥPPb8aJc+CnkZ&[zzlVbP|uۊ Oej#) ={e>|n} z?2+3Wy H鶹< խ HOr%nHi:h)yhӢ ֥4 9ˉrY¤5 lp Q`LD<&8DQR.Ԉ)&H4Bc$7ee."s_\Rps]u:vme3c&mHnk xb;n2dXVCˢ5i#wJSiFC,UȇcQD>*mfnf37K&P  GLAD!]>kd [!v` _;-?dHTRxnijrRC>s,Ԫ@ 2eEYТ_3i䓅ڥoC_dhN' \ s 6|3Jyhe d;.2VA#NNRz? 89U(dcB@LT' 6 @H/ '`)5FC<0CQrvJ$ `I)]58MowN _9ckpԂm/޺`ׁt7_{ @Qۂ)[;<}#VM?cW  Ùӳ *n[T!҉& QŜ!"rTH$+d\K!( p 8'2ca2 Q(90Ar;k\˻kNlplGnQܘN 9v÷`P2a.ʘtjU~;1RVKZ#WC՞\}Z] uǷ z}_˷skS3ĭ%d*T +=YeR81wW0@ 3Nq 2áǜPrU5&% ڥ01j i (m Sg1Q[y|(lIکYY)%J!pX _CڏGϴ I^@] 14kHs]8] ջ?oZT'l'01^P"W6kfklJ Cf}4=b)K:vnYWd&; e2wVT~Tl3[ 9|Գt?k*6DE-ev"|p@!%T~9<1]VtTÇq0ɶ #L7<mj`bq`g]Gjޕ<uCeG%:iJ5ҙm]QZB*$:U}zxl&jA? T]FYT;#+PKDvu@0vg"(̳wmۣEWqj8A`ò 67a9=C4D4&Y&Rq!Pbt4qD3œ:-e=Kv= U$Y+]D^):ܚ*bHNf&&sVFbdrɒk؁Rc#ZtbU2t볣 ;*4"1k11b,\{!٘0mf%QQ+׀H}p=6H#SIh0ϑpvT<2e]\v| $(5I* "$߻+&ɴn2"sM&k\*}l"Z 8%/ r]PNu4j/-3fk%M쏡4ӵ17n\5͹ùW]XC (v. B#9b`0߽UHewr2ަCjQQ6fX4|[h U̓APYAV1nZCBHeQ2/ h?>Rv%k""JErĖ@՝n101@D8` !m*;Ҫ,j=L]v/P;AIQW\یLγ˜b+Zntk~]y Z3aIkYxцV_eo[ 6"k3(!hd9F2B8e}?C8uqnkZ[7{Xfx>DX9n8GamS`& Jî1(a~z v~ pϽ/{ۦ)0GkS~DQ͑]K[[U~V!ܾ?GdgIdW:+\N]dX,oIV:TWvgZ 5z󷮼]Hv/,3)ϒlLΡz3\s1L1xŧhuG{Pj"}ݿ'tRX~h ES]{2ϳۉWgC=-ԌU3=hqn Ϥ͕Wc pH1wĭ5U=X'Hyw NqFΝ:6/5SjcKLY:ܶŏuіB7+Vz?Ug[(5"R^۩r"b[;ӻY䶬i3lj0UPnc? ^̍!'3)UWStZrf" Df *QaC̣T!◻,qgR~czCwV^TT``?f iӮSdVuXJZo IDAT_}teCIˠ.D${iBp_K{ݗ=u"իI=5&i5dkMS:8iߞAr#:`iita ߲y-PQaEhUM`$5<`̱xsgǃ g87= 7v}Oiӆ9xJ(ȼ5[Eb|l/y(3vڰn'op.EgD&`0ugOl.lu\AĊmT-ig;k[|1*8K;C# 'yHaH Ci#AU1CDvxQms,}x-E;]Qu6&U)e,^^azjZeOMTi.l O_ϯ_17?otM'k__˿{o}O;~U<{%va>o[nyԣvɟ7}7jo83A bKP@cY"U" Lv +D521D xַ>{γ G]/Ma^@fQQ0f3C)$\i}@hedQ*δbX9P!},Vn$5'9d&4Ѝ3wU*y^aEʷs HO i0rSs1~KvqiC%& Z~ME8mzfێ^xϳR0 τԧ=|w_p֟s٫'~W}۷]uU'k} +_]{W?~++wweP?r3x#q뭷vqGG)upfY2lV3HH1$*@a0QbіQ4bU}o^=vJZ^?3z"f[ҤvM2>L͔uꋪ[nĽކR,PwD1re Fe,KʿhYE$^kS N'qp*ܮ!˸Y׵ʼn>֯7¾RCλ2Ƹ|{yp@Dc |#pW\._b_C&Y26ӥV$S(zi45NVLY}}E/z+^qx{wS$=p>B_UqP.[L(O Gg0 7I+1/X?M4b!@n2~*`z̈)4lGWߌ2U5K6Z64}]܎p[RG$t 'nc;uzk8 -v*˙ ^{T~vMſ ~4 *wc"|C|#U3 x7ys\~:]wW-oy˷|˷G=Qx;Ο?x_}믿éT>_Ļ~W? NL`*PU3y%΢bܕ0Ci"x1n/| Z?~t+#&&(YVL\&";{6Ӕڒ:ʴRHxK=R)aIHZSSу+k@.9pdQ.ZdkƩn߂Y] ]Ҥqk[T:zKk-iTUhCjPy~Ia[1Yjƴ Z_в?m۲!я#=k4w@|P9ˁVS© uekGFK%F[LR5 A_`,hȆD&M_ϟԧO5s}Go~S(_ yOncgw󟘠믿p*(8~4aOθp§?i/{^ozӛn:<.m} AHEvxT S`ɋyPR&+ yg(w pD^7dV_S$3ۉNg5/fzq/M.U~se5A8@ O=҂5j*,qג)5k5fI>v:S{b l/j{~$ fùY{ VˁY+i\̞TI O/$p(}B5˜fQD6qThn*^!iȔc5_;s緿{?/Ͽ{#ȟ=y~~d?s?oo7>wZH'</lYmAK6t`o;''oۿ? g8$KQU]vAIUH]ڡ*Q\QUe3q N 0wg_w\qs=7o n1̚*<%QOUЀ[PL]~YyyRW 2T6 5*.#UfEMݨ;QE-@A=2!0Iy9@4 s/tbܭ\x;>Y];} [[RVd۬KWTS Xh 4Ig4=No|^]{wqNj_vk1ϟ|_r ?O 3~F׽W]{wo.^nҗ_|Dlя~W^( ŏ|SSJ_U"3?я뮻_?яp{oo{s9nˑG\RwEvO nwAV{UӼ~*eEYD X1q`{wL73|{h GsJxxU?o3L hqmt ƷcTrD(чB U̳i-s_s5_X~7u 0#6Mafb 3Y2qNtw6tQ-&MUp60`MEV#[~%IeK9gҖh#HyB;!GW)U&_  9* nϜ5BlՅJV÷Xf%DըV-\hWDlDQ=2G}w~rIPʸU>oW..gf hQA"2n0TTM4L^UΔw=o۟5մV$eDà@ V T!1ퟶ5ڨ\5rվR}{kM #"4h~,kx4$|x aʳ6/v֜(L3ĦE`Trُ46y\+Ü0(V_wI7 ƹ5 8b$0JXu*y a4*?6,w$ " ͘^ h*QOgK(D5F%U?~om{kk!}\<|7x )]B2b!d>!1תڅ~v̘.|.sB 'Qmi&>0$ 酩Zn w/zW\1=Bl40eQU5y[7Q7D!.)4hG My7JYiBpuɈNqc=>]BM7bIO9V<{7},l>/_-4x!8Z3U2[un-||QWSkQ7hVUÒk)7 Ltpvv?s;'WR\36Sݢɰ*L`bBfO~0!ɋ4݊LPQIe'cj fj_i)I 0V1Mݣ Bfш @A g9+S g8>&XLSYYib ,/8'UL2)Dt6eM5"2L)ʹ%F_0w HZm]YL;[c?θ@~e%7Xdݟ)F;ӷaA"%܋6YgC6}9Fe?;kAYuͰ&&Q(5\:mԝx܎Bl{;VqptdT^)plW6 M?ӻHx#p59a%$cHBDmPQ.'p9Gb"6B\MH7u&{!"(Yuy@LPHA5@  eo27_4k/SƠꬦXv6 "&Ղ.Ҁ|"2V&RhgCM46!LEBT^,M6HEdt,ixJsL5YFD q~W3z_yy]dY ?ŀ,3HT=TE2ڨ)hP uMA%NMh E#RB͵(Edcl:цe /ڞ)'TJQ)Xцjq% ڋ6NR\7qyZ%-Zmkq9Cq@[;c v]LOg2XRLݎ@ kKI Ԍ-l.ė*R*]dW{9x\d+1 4]nHk>)S1OE!അH|*I pEI`B!b%# }W@4җ8 |tDDdq_o#@( {%%`B2UB i$w}B&^TC(uRX?ʒ3Lq?UEK [  ނیz/G s\cs<@`FFYr221ΊP2,b%/. TFnï5ĥ[7Қ4kwsXcLKg̶ 2Q62lAs.8P5`҇I |ٔ N:`AH]4"1 %Q&[Dr@)[>*kr [mb2I&SM 0Z D%Q^g8T)zQ%l HTbfP,BDc*' 1C%M A8P@[o࠰`{xfTt 6CՆjMؔԽ‰-Ibp4,/PnԖ/B$J;{n\Y] H/ mTB'1* TytIց4Xs,,I%~Ol C ]ΝH KniK׵^ه;t ukTȀ"+;4#jO$=Z?k^@ZUeb Ae IDATf4'Hi٧m Ifky˽J|[!ĘBpESk&zU谀łSaWN֠PZ(1(\$q!(Os` eJ -Tr,QÙ"Y'͗(D#@!<$JjHbbaLVNU+fCL&Pd6% &e%@(lIUfYkm$ƺ@v4'тqCibR "NVybA-4Uתʻ2;2nw}ڀ740I d3S;-",p חHlR_gZ#Qr@*WDM@D (]<́̕YtDāx<̵ W:mxяRڧJ dR_(ܔ1_%PS Hyղ:2A޽ý.o]nK̢̏3Tbk1k$4ȴRD00mLE1l5:QCSU% 50"UQ!|g~Ѱ5Ձ :솒;cIPZ-Z3@OtvT1)U_.TQfVxmeke[o;pZeaɂr R\iEc4P M|0ecr*eDUqK`}dZ [bLHD%hؖP 1aC (ϮӔ2 .(`}֦/T}qڰ$k]ft]i;1 :TZԔ+789]vv@v04;mh [3xP`Beăe[z;|+;!Y8h<(ɖ$]H+PjvS&{9~t)[.44rkVÛQ;W22xڔ7JbDp]-BNYz蛶D?bU?F*d 6*D悓@721@!BuaQ"2GYDQ=ZfzRö>4n y]_c EF@9a֫;NsX},*F#"Q\#nH{ǟޔ{/ JUf)bBLtA|ԌC#m3SÕ$0]'$g"Ko"45"Yg6f`T[ǁ9% N0=ZL^cD xI !H#x~Ùhr/*T3s&AFX`_蓴Wd`^=|\8mLpJ +!|"cTlc.xGMZxBZ/\o;d4;bV3ǤL8IY V`Gf^Z 2S)VvZt`o\Ehn8=6CT{ՑCg#K!-j&\,ύOlPَ>UoURjUWPxCWF[0NE"):\iGeNl}0ÎzR=mg7un/f\Vsj5&j:]E`qv)@%% &AKES &L |H3J,U!1x )3CۋMcL $^fyjV3;g E2"夠dӋ35D!;k}TihZib )$ P RATT 4pD}1P%ZQSg,w﷭:C'%*,0{%m6D1oe{Qp'EZ!|߭B`9b/]}jxpv6Vb&|Gq Ӥ0LU0mpHǤH(@.}T$ fV]6U|&{2J;{1&ͅk-iK~= %A9?v H~7rT6NRb.H@-1I Q)êQ-=3"ʸf֭i pp8M4ȫyrXL)}UMM +A!mF!mxVϚf:23>RF#4L|&ȑ+k-I6Lj}t5[#1$FҴaa+ DD{F/,V^ꩍc E82ƠK,@EW|."%DQ͍9<DZwb6aγ[\ et83h'"r^i"HDܭlDH+ݓHM@gWh MlCjWUň ` 2G2R(Ti#Ѽ( p'H/d_ HR4j.5 Rr-DsHa{zBOsY P=daVɎ )ۣ2 #Rjj$"ۋ /4}WGT- .?^(t-qUaSˣe ԗmS|#y{681Vj&v@ xim'lE) Q4k]֞J*%Qp֒t)爘 d;#J|W٩Ka(JZ"((zqsĈt .^Th!*u3 gibE0_T";!|@J31RF !@7lhB)~S%p% hDUB'YK$/}#MNYEdQNrC{ANLpQK1FW K9ө`/麼6N. 8%T+3_yU5 Z~ =ӂS1ӌ^ӓhiʣ< &>whO}*+4=Vz\MO'T*-io'zXs>bۯebU[Qieo,M;Dño >*-/z6_wR qɇ#"+8 *U*./R(8$icTJfv8H*by.0 b_; H0 sU:M[on\ӌts^N]B+].F"7: TJ( 33/Yx""wdv2T=0F^w ?6kXD!X1EdؓZZ&0dRrM|ԡ_aQ7jkոMM)LPF#꣤.tK ` (0O,ۋ8HH aHDN⼡U9UsI4]˥*aDhLtӈ2%&( hsMS#ɃYghԎ'S R F݆r072S8K8,E⬳N6GḒ^ipR?wWZS FP[9n4#ͱZJ bZB+ƛYA6*h$Ud-r}5ިZV(i*z!԰qu>b`.mY^ERh.ӍD S8(8g+$3phfYV2"s̟HP9jbDJz/R\ &w͝pOxkz1NVm.V6}W% mu(4oK*)Psp(wZEDߢ9CfΉ첂, Sn]dr/"}Po.5oe`~DWMUP PP*"LJ<&S)*aYvo/;,s/LO7h8֤< ^pTҋ$]a#N4RV_[$RTyYU;HdpB011Vh* S1$!5%hLà"g1:@ @I\^-U0ʌ\"  Ic>~@)I(_HDgdh|)UΊygwszIAi30 8kdW Q'+L"OhĖoZi^ynUu/ks׀1 `)c"2 ((bhPC %&OTb41`ŋlt|{Y̹{fZ={͜5\s3űQG ߊ-? 7dQ|ԧW%Hula mXO)0,RLkP3X Ԅqܮ9%tɛ֏6v+p^km5wLots(iU0:,[wI &.M0d$#%(ٶ)B ) bHG /)YxR` F\b0xAwSIF7u8fD^>IHXW#hIzx$%*_@$zO[U@C+.Ș9rr=FK( ]XIsXYqqЛIJk.O9C]'b^19.. H؃C8QaC V/B*/gdfK2d_{)g ͩ "rvXPjG4o@pSQvmq"D=$ʸcij爡x涬iZE- qgк[*VR.~^h¡а~sVrBeC X_IS:i){*jMgyYm%zFTK")z%iJ.?̬ ,5J%!]w$mضAn aX#?8 Ƃ :.{k Y0طD*K @?ΝHRAH͜N§{ۥNȜ}"MD2s 9j֝0e 9Q?ƕ@wsZ!dsYB8 H 6evлc=]*HrZa|L+<y+-}{IL`FAIjdžY$p:OSW+=ף) K[=W&$ӟ&jUREǤh2r)QD)F>-p=:rED;C>X7J2-^33ɑAr -F)q#6@X9$IYDP"AN ٬FQkS@a!u9&;G2x\㓳B=nԏ:Aa0Moy'9 :'8@ѽZ ( gőDPY!i)#%8HFHÐiS M0#?t JUqugEimD7b^HȣTjt+otNX4IAZWX@ȵ[17Tx,bZ L'z3q|h= "q.s`Q 1FuKX>R FSU.ʼ,V푊j 0꜅#stX*;=UhTf "$y&i:O]tD8':yJ`F*0gz"& JI)sOS1= I!w"15!DG9ROR\Gv|".WEH[?G\!-yN5Mcޭ) m`w<9 arթvY9թ4LL`hus=NGQiINq.i9a{_N*LtP,h Y#PW6_k7g%ywaXl^3cd(Wk+Ih\f%uvůͅ*-UZ1%.jt'($j( zR#@zY3 cļiu LT崮Pߪc*`3CcL 26y)Ys;PW.*fUd[TGp ?%]-9 0KBDu] ^Mٜۖ8^ P/^Zs\جtvHz۝ +]#B @駟~odtc[ne~4ya& +&V G ԙ! $',)]k 509؆G%p"`q?)@os޹jPٸ. ?s,w@ 20jH(V 1\ Q;[i5_x4;g#aTmѣ%`c6R>gZG9䐣~֗[n͚&b{3]pdw=?cᓟ;II4eXd`Iv@ A-9`X{S?7S\|UJ,&cD~=J~=嚮;atDrLIQ|-Bbbؖb,>Rt㿢nԷ7`KRH4#O( s@*t#DAكNpyTCvQ@B8]3CHmCSS񞺎D3= )e5: $B:G]?++x;&C9/޷px'p?Ή^=lƵ6L nr%ө0yQxܕ- ymG{ʊ!=L(S4d+ R>LIZ囦XXflKjhe,Akh d)l~q[]rroR0q-GQṇu֗~+^뫫]69?8_g?-o9gkz k]TbNsK-Hɥi *ਣ|/첷OqZS$_ՑQI*;eK]iKjJc]*(b(MDakTl(\>2oݚQ=++^f:8;J8.H=i˅ V%;ޑ@p' )bQgOHRC*P[Q\!7՚0fhpHAEt^K<_TbS6I|: /;8{//觟ׇ}K.9~s#裏Os%\r-vF8G\|??d|a&oCpv6xDE.z_yǍ5}@ⵁMhև~\Br)^n :c$S',`_ՈRXx@;Qmoߩ,J;(Jw,D+˚OOrțz*_>˯)/O]֚ 's0fIOIKK $I0|_ɨX¸謘-[|S;짟ԧy 'FE1ӜK˯oCJѸrsFHBɹ;*@0J o17ya+c5j ޺\ɋ;O9:Z3)[!5 3բsHVdt@bw0'Pjc3 Gx 6n#;`Gu.{rE9@(,:DĜwGiI5a]wI'{_?c9cI??яrU/ &B2)RgB$@@zŤ[޻ZHG_u9T;\}DI 9v %tޙ+-D\#) $OV۫i` 6tEX51ۅ>)D?XM$58^ژlƱ1Q5|.?Oܹ+]ke0⮙ ^n0 ;oIà_E8wz_~'t^{լQG=I'oޞPb}UǺphbR薚ݘ[!ژZ0F9KΏA'*Q|0~ ^^o58"}NAՔ1]kԛ"JfQ@-t403D!_r=O@X!HEX-RB&Qb?RṅniΝ{w{ptM - 8=;n]I$_ɑ>5"C$p TaflC&:',3EԍzGoV_rG!9}`ʶxM4Pmx^GfT6Џ/QPc\0fzLC׌/b Q> eDj`ETQƚͧïȷrT+{?yp??sn:LنH|Z|Wz]X O9 /o}(H"bSKZFW޾>'~qd1-1z9\Ju˗E'EKHFo&R ʱ( b X fA`ff=12:w5BP H4g҄Mz@Q} 0ʤC'VTcBgGvI=!jbglOx"/?|m8CO>N::{Gq뭷aǏ=ؽ[k֌CD/x \uU k棊ĎD9"؉@6'đWc=!mJ":3Զ(WCtE% ,y:4?+gRd^'_eRGZ}b mj^̈W2a,F!N.@lI< c)c>ů9!lITeJ!TV6n_7&, __x`{׻3xbԬ~ӡO1їvJT0QJ,(H|=K.޺uݱcǧ??l g?|7oVQ)k7@"I *<9P~@B|9DUNS-:/ GXƯ+.NOo@ Ho`ٮ`߭Fa*HJIKqDF(nT]ɍGuf, 1CvK 3#+Htwl%DU9qa=ak =ꑏ:_uzы^O;~?Ko۶/+ܾ}>3SaiO{{G?/ uQg}6;SN~# U:眰03(FC`9yb^\R} SB䨟qYOpvZDԳ809єy\4#IzGh|q PI՗@QÊCQ-SAJɣf UYt,weoW/ og?.zs_m۶mݺ  O,̱l,n1lX-`ՀC{_xOk&08'7;;5``-U %Ɛ)k,1A80@0<٨a{ޒiB ]EmԸ P/]ݳ&v=u1ll p Td0 %n3G4Z H*zIŎ5ѻŒY{xE7FB j7c>rq`nY>_y^rIee^V'U IDATկe/+۾}+^RcW s9g!((֫BV.ss#6 ,=8rH8G+v }{&"缃S֫tzˌ*@p'brE3&V@A X.]J4E1|uo>H1mP g;֜+A_uK_BK|Wr-^m۶ᆵz\v5 Ap7Q|ץK5 &mbWg1i*?^W+뮻^vܱ̺ҋ.z<5W/c5Kc_P#CiU49VR {=X ԈSD_Si c&,׆璑[{:K4ˋAef abFU_H-:Cj$@gjT|uN} ]&{Q+Tg^\\Q!xJ1ޓ2qRZks{z/W25_W⊗%M֭L'-ꫯSNIh.#){ڜwG+$:HӰB8Nն#^kcXT!"D 5&0ɎU5 [FhL6H5GۀhiDL(PVGrZƢGޥoa:r_tBԍ .|KNw/\vqyU_n20ӀG->Eu朡IK(Θ-[N:D}?|m|x _t]pK]_ZRNn$GJd#|Y_{j xs|PosdCknSu}Y,EF&D*Zr@.Zpw#1Ɖ)R 3e7B/ָUq7ΎPr|v*gk^k}͂9R^ߒw{ǿdj?q~XM J8VVVgul6;~g}6 gIV[`Y'֓'3=K/v BH^cbf5\5hVU96B< as[71TTUUǑʏm̳(dQo$r6ud8Hy<:qz(mHMITژo>߸Əqǁ>#?8!ޠ!>\9̆B-҈`CF><>M3<}?b0ӟg>ٔA>Q )Qw ;a4 F5-(?L:Za|-,==VԤ.ƞ aERnJF mcm]ebDU&ĩ ^F^Px^Q)(U9=EmJ9} 砞 O9Qsvh,l6r@G`*|V,taf~wNؠc]aRT$J|iTq;߭nEM`^dBBk=A?졇fYOί_gf=Eg&g vZ&DB~;q\ od0j# -H2zA$e]’`#.c*}QE j\e;hc/"/;K6HؕnFv/V{k4-A5QNKcũDjb \a EQ$ژRBχM|Rvj6ZE#'[- #Ex,9g9,ى9GSQ$\ &X9߭8}63-,eS)?]L J`s=&RjەI`frGrQ ;VnH' dӌ'[G'|Ν;?[n=E/:∟{?A= cr)7||I*.'"Fn ߝwޙ>oٲ嗎=ȧ1۾\yUvMuO:'?yΝ^/}K-M8G_Qfl5~\J4" !|;TnDD{ɑx贘[ٺ3 qP%#H @"&[XbiYR|E$Ωtr]&[czafAo BopR$ZfO"oHy衇~_tPz~[zn^ /~p 'vFry=8$80`C(`{J0ЍЋ9}fdz"]ac=r,EpIEG!/YJJZ Xin(hHr݌6d:w8Щ] !\b`ЭzVs?8N<׾7|- 8{w_rqN;7uP&LnwT&ޯ#CMt(s:;wnOټob 1׉bG}"gpifRI:?opuaƢr9ݰI>PK;t:U@~iO{{/>+7N=7Mst{qGx㍿k13 T( ]vg}g'R{ =k!fH!A8p9λS:x,H ;.]G+[huկ:t@(0ze6 Ids;Dn;f6>.@ 2?̋nI~!]h`/T$e])l/QgQ3Xu依im|ck^z]wu9oO}9s)ι#oFKj}8묳>۔ik+P r$"lĴ!ޡGfH`W9u I0 ![wyBK903+.A/!՟iNu% BA%lNAJvܥ}᠑ yac2]F5u~NhѮ'< sT]ڏ|S7\^{x 7ި???`Ν_'휷as|(Պ- /| <㯼*?O߾5k~x/ ~5\>g>k֭J36! ʸcBi=g(~#Kz^]4zPXjM(K *oS)ϸFDNNZbt^: O$"=DDyNU&Di!8%Rc|Q`/C fE`z*"^KNcJKH#Prԋ*?\q5G?8ouݥ^zi}k_zn;+wx+X4tqO?=󲗽lGD[qAo$&Q[DY\d"´$r>Ϋ&N5QS A8Ѓs++^4Rɰu`S:XHKc9fD$ aʖ^̪VhjF) eA&sPN|; Dڔ$"IJ&1G)['ĺSBOȥkc%MDB` q=!Lc[Q5r&UE)c]s}+ /hW_}urmC~/}KOSDizos+N@p2^S(`NTthYe0FsY_s'B|6F]>+ڔ`. T0fT%HE̢ͯ(]K4Wj(IɥH{I$İ[ÍGiʟ&&%է>evپ};}{zBw-K/N5ei#ࢋ}cϿ|@Xvq]w8c~YϺg*nj󿭒[Kd<[mE i:ʏ$,OBBbYWD` DؓS72=Sp4]~"a^s/UYiԶ@UR$:$Au]w)|C替կ^y/yKO{w6 p^|)Y ,LEțY;R z YO@P |EԫgڅYऎw"2Z?t\±RzBESOR-Eu<4Be[>C(V%?G@~K\LT-${fѶP}a2̓>R'=g?oxO߶mF׿aڨZXG DP1!rX!-Ɗy?z_3f'gvDmw|x#y! HKN*&e0XŠ[OTVNuYO3FK+=gwJ |1l"BB/q[G-wpSבS ԭU߭nŭ=gL8pEza`5RGoB/➅3vxnA {Ymv.s~4?>gDs`T,D!Q$tQjW;}NLVoI"z͸q?ٮ0[ÌUI/ 98TlAv`M e4\-UFה?f۰!h0Sڷ9ZENUw_tְi֤vѫ>'tⓞDc'tAtMW֮~p睟g>wxիMʟɰ rC~0G e/~K~"O~~ޙ'p 9}Cp/)/y׭[ܹoXж1< T5CyzAhap nQ|\w2 !ʎëVE@Cq POpd=kjQmEb׶I1 DL+&8!ș3$ ysz[@c4E )ܦoRʰ> MQUMaqF>@VxVU 9)P{N*p9x9s[iz[Z[;ݵ҅md-2w|;6RҀͦPF*f/}9\=?HUvwQG=#?8l6{Ϲ`Kz9!Tðޠ0fFҬToZ3r;Fho|9pܿ 9r1XgPAJ a8sFι3'L݊wNQ`98!g k<u|N<߮$aKAFj>g>;v83%\r1馛^Wxwx =/He>۷r-<¢;uL*Dݜz "TVI:O*pQ?;+kfHEY]";k6爼Uȁs];K+ B_N;ϴ?g衬C):8zO2giC":U!rvB=On3((" ӂ57k+?믿[oݺu 'pa?}w_s͵;v?:C۪}{"FcF?qܳKWnS?S)x?oȰ%sN{7^MAo9C9_Ϳ-[/8 Gs=^smۨf(Ur~C9E+,*QKB Zk8``IymYxSb%j]k99xO䡂 Vk>߭8{g\3(*JnyO$5GC=A}u8:O 0q߫MW$,P"Gv9p<^:"rv^2kQ RnVxpg׾3''x;qv'l>X2t*,:egOj\hOأAKd2'Q5720Qqyi[@N\dևޜF V9P)[3D">EzIЭ+XPN Ne䠾YΓ O0"k{],km;XU;bl-!&P%EOc,>=yO?[(m0lJe}% qi!Ձ8 ZoLj($'Pnj(/@u6+*RWpD`ь>Z0{$ϘM,b vc[+0*䆀$%08l-&:%~lP u+w;KM%XOXׁF?G s3^Uըe4RF^gMnTa $K&vBac9mzlH[Ѹ+0IDATpf!ΦOm:"E< $9-@rf)71{[z=iCQ"Bd~58S0 YgB azQh[h+j&*B8&&`o~hrnvUPDfuj 656K.H!9(f N,\qN>L|A91ul<R9e%֌lmANu2ZTFϓԒa͟דaTfǠ +*-sN>ģR&K']#fa)Oޓs^\`4"g'_a ]4pfAƩ]0Kk5jDEEa1a0l 4jR s(&9ׂ\6A)R̡CT1_l:ِVG[rdb^%h3Æ"tc 3 =!DpNzd+[<#zbfly1Cd]0Ebq;";1"qދ A,O1<=5dy/* HXL`9J]Bp39aϩTcdk4@B`$ @6b$W&Ն<& APk1[aYR2 h).flDݭU3m#MZF@BX':ǚ/3y"BQ\#kQ2GZTM  Vfԩ?D w飌ŒQPDˀ+ Q㈭.!KN97R3HZXVIVi޽R{)~QQ[&]jNBZtwOQWeبuD&UxY4I;`μ,32[ƄhSZ1'1"nl|k(ͼؓۖh! _Ae\lGHE-i@*#KnDʊBP =" #J@tH͔1t\%P∼wґcF Q&hüxʼnIjM1$"`/mY`OT@y"䌰8/)07;o,f3PE" M!(Q_7x!%ΔqbH~S+4^N&25墭x8EWݘ)St0eAwx-RfN=(TǛWPG1Gߩx.jK(tU& RQ̿Ӓum GNxba<3 TjZv|T9@L;VRx;̈gkz!C@-{;afV@z2b.ݠbz#xT0-ftSgQ}8j5̵YkS4;s=8JBC<Α+4= \%WI>FヂR4Sȑ-*T0yG 0 ш]6PsFh#cUsߊ< )V|\t#j+NXyCJ⩓cU1< +j65wOe"$ B~8ʶ/>,z46xZ ?a[Xdx_1soX9:7UR[Y'"?0JKXx6$=b]iDDg糤0DbH &-!G&C K4(zbx{ v1AYa;ȑyE&(xa !2@{a3yEjV?@!Sf8Y7Hld)*{eQY&MA6iqWda"+BRֺCMQR"]rb VL̀FzJzkNt7QdEYGxh)P:ĎO7Ab%J;:lTcW dʽBQ!VfݨbL0W\$UG9q9 Foiηs`$&9l64Σ59ѡ(vx@O*p@PB\b^V"J9GT;@\$2P{zDž50XD,d(I[ 4jn%Ίj\#" j$)SlA2LZmz?ي!I C-Sυ_iQ 6nhtJq?&620VJ0bQJ.S3o~'+lwaqH"? rR!ڐmyƩG׍:&+1]#aăF:i\87QwqIYR*p=qazG_EmՎP_TٓrHn>nMJG 7'7u×iHƜ~&bCTPtڪ;ܨ~jIJ*U L /P4LJ |hsRFʆCO^ׄPIZY +=H\2,?$:BV)R)_l,nMD, !Nk[vD4PF4%o)^5agQHY4 &bXspg05-ּTX?2؝,(D#$KE }]t 3\؊eC`)8( HBT̰CD+e2Ɋ e,v(%b[^@\t|Pb!ڌOci$L9R~we-q-x UAZ Q[_ )*\5BStPf/zwy>nJW3(2Ö+-=kI >hP4МMD6m맠i#_CB@|wvYq.;|Q`fp2816`Sv8QaQN[OJޭ0N>bgX?i7i=hs^سlNRTY U#oӆ9}QZ2J *X7 {E酲3HGcw"L|$d͢/l %TV%HsRg^9AREt<Ӧzo p_==JR,{^a9zhJ=5 *"󅸥&Bî]9ǘ^K#ܪ,& 8jJ>7o|%q>k ?j>uo@9i ³+{\;!n?[zB4ދdݎ^P C$ScJDH N)n)S!S휋Gfd Fיn"|Aӿ|J`jSvm^ή\LIgqb$E? bPFE։L5q5K2r:*ؔ-Sa[, CVNh"x(2 a-4d1`t>vebոƱܠd췥<|#eŚ,5ϋS!<It1FnUM\]y _hE&(1n' bdH@$c1Uѷr͓q9 )]vGX# J`P<"m&(8uVTp73w}[l]Y=GFn R7ǬCl׮].x <*UUpuΣ?r:\mjv/D;ax)3J`#18H0S@rhԨ@IʚцYgo8xo3U&&M "<)D1I9 @NLL}Zsw>ƍe1`hIENDB`livi-v0.1.0/src/000077500000000000000000000000001457505274000134415ustar00rootroot00000000000000livi-v0.1.0/src/dbus/000077500000000000000000000000001457505274000143765ustar00rootroot00000000000000livi-v0.1.0/src/dbus/meson.build000066400000000000000000000006251457505274000165430ustar00rootroot00000000000000dbus_inc = include_directories('.') # DBus client interfaces generated_dbus_sources = [] dbus_prefix = 'LiviDBus' generated_dbus_sources += gnome.gdbus_codegen('livi-mpris-dbus', 'org.mpris.MediaPlayer2.xml', interface_prefix: 'org.mpris', namespace: dbus_prefix) livi-v0.1.0/src/dbus/org.mpris.MediaPlayer2.xml000066400000000000000000000015101457505274000213120ustar00rootroot00000000000000 The number of microseconds to seek forward. livi-v0.1.0/src/gtk/000077500000000000000000000000001457505274000142265ustar00rootroot00000000000000livi-v0.1.0/src/gtk/help-overlay.ui000066400000000000000000000060461457505274000172020ustar00rootroot00000000000000 1 shortcuts 12 Video playback win.fullscreen Fullscreen win.mute Mute win.toggle-play Toggle playback win.ff(+30000) Skip forward win.ff(-10000) Skip backward win.toggle-controls Toggle controls General win.open-file Open file win.show-help-overlay Shortcuts app.paste Paste URL from clipboard livi-v0.1.0/src/livi-application.c000066400000000000000000000305731457505274000170610ustar00rootroot00000000000000/* * Copyright (C) 2023-2024 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-application" #include "livi-config.h" #include "livi-application.h" #include "livi-mpris.h" #include "livi-recent-videos.h" #include "livi-url-processor.h" #include "livi-utils.h" #include "livi-window.h" #include #define H264_DEMO_VIDEO "https://test-videos.co.uk/vids/jellyfish/mp4/h264/1080/Jellyfish_1080_10s_20MB.mp4" #define VP8_DEMO_VIDEO "https://test-videos.co.uk/vids/jellyfish/webm/vp8/1080/Jellyfish_1080_10s_20MB.webm" struct _LiviApplication { AdwApplication parent; LiviUrlProcessor *url_processor; LiviMpris *mpris; char *video_url; char *ref_url; gboolean resume; }; G_DEFINE_TYPE (LiviApplication, livi_application, ADW_TYPE_APPLICATION) static void set_video_url (LiviApplication *self, const char *video_url) { g_free (self->video_url); self->video_url = g_strdup (video_url); if (!STR_IS_NULL_OR_EMPTY (self->video_url)) livi_mpris_export (self->mpris); else livi_mpris_unexport (self->mpris); } static void set_video_urls (LiviApplication *self, const char *video_url, const char *ref_url) { g_free (self->ref_url); self->ref_url = g_strdup (ref_url); set_video_url (self, video_url); } static void on_mpris_raise (LiviMpris *self) { GtkWindow *window; window = gtk_application_get_active_window (GTK_APPLICATION (self)); gtk_window_present (window); } static void livi_application_activate (GApplication *g_application) { LiviApplication *self = LIVI_APPLICATION (g_application); GtkWindow *window; g_debug ("Activate"); G_APPLICATION_CLASS (livi_application_parent_class)->activate (g_application); window = gtk_application_get_active_window (GTK_APPLICATION (self)); gtk_window_present (window); if (self->video_url) livi_window_play_uri (LIVI_WINDOW (window), self->video_url, self->ref_url); else livi_window_set_empty_state (LIVI_WINDOW (window)); } static void on_about_activated (GSimpleAction *action, GVariant *state, gpointer user_data) { GtkApplication *app = GTK_APPLICATION (user_data); GtkWindow *window = gtk_application_get_active_window (app); GtkWidget *about; const char *developers[] = { "Guido Günther", NULL }; const char *designers[] = { "Allan Day", NULL }; about = adw_about_window_new_from_appdata ("/org/sigxcpu/Livi/org.sigxcpu.Livi.metainfo.xml", NULL); gtk_window_set_transient_for (GTK_WINDOW (about), window); adw_about_window_set_copyright (ADW_ABOUT_WINDOW (about), "© 2021 Purism SPC\n© 2023 Guido Günther"); adw_about_window_set_developers (ADW_ABOUT_WINDOW (about), developers); adw_about_window_set_designers (ADW_ABOUT_WINDOW (about), designers); adw_about_window_set_translator_credits (ADW_ABOUT_WINDOW (about), _("translator-credits")); gtk_window_present (GTK_WINDOW (about)); } static void on_clipboard_read_ready (GObject *source_object, GAsyncResult *res, gpointer user_data) { g_autoptr (GError) err = NULL; LiviApplication *self = LIVI_APPLICATION (user_data); const GValue *value; g_autofree char *uri = NULL; value = gdk_clipboard_read_value_finish (GDK_CLIPBOARD (source_object), res, &err); if (!value) { g_warning ("Failed to read clipboard: %s", err->message); /* Not parseable as file list, try as string */ gdk_clipboard_read_value_async (GDK_CLIPBOARD (source_object), G_TYPE_STRING, G_PRIORITY_DEFAULT, NULL, on_clipboard_read_ready, self); return; } if (G_VALUE_HOLDS_STRING (value)) { uri = g_strdup (g_value_get_string (value)); if (!g_uri_is_valid (uri, G_URI_FLAGS_NONE, &err)) { g_warning ("Pasted uri not valid"); return; } } else if (G_VALUE_HOLDS_BOXED (value)) { GSList *list = g_value_get_boxed (value); for (GSList *l = list; l && l->data; l = g_slist_next (l)) { GFile* file = G_FILE (l->data); if (g_file_is_native (file) || g_file_has_uri_scheme (file, "https")) { uri = g_file_get_uri (file); break; } } if (!uri) return; } else { g_assert_not_reached (); } g_debug ("Opening pasted uri '%s'", uri); set_video_urls (self, uri, NULL); g_application_activate (G_APPLICATION (self)); } static void on_paste_activated (GSimpleAction *action, GVariant *state, gpointer user_data) { GtkApplication *self = GTK_APPLICATION (user_data); GtkWindow *window = gtk_application_get_active_window (self); GdkClipboard *clipboard; clipboard = gtk_widget_get_clipboard (GTK_WIDGET (window)); gdk_clipboard_read_value_async (clipboard, GDK_TYPE_FILE_LIST, G_PRIORITY_DEFAULT, NULL, on_clipboard_read_ready, self); } static GActionEntry app_entries[] = { { "about", on_about_activated, NULL, NULL, NULL }, { "paste", on_paste_activated, NULL, NULL, NULL }, }; static void livi_application_startup (GApplication *g_application) { LiviApplication *self = LIVI_APPLICATION (g_application); GtkWindow *window; g_debug ("Startup"); G_APPLICATION_CLASS (livi_application_parent_class)->startup (g_application); window = gtk_application_get_active_window (GTK_APPLICATION (self)); if (window == NULL) window = g_object_new (LIVI_TYPE_WINDOW, "application", self, NULL); g_action_map_add_action_entries (G_ACTION_MAP (self), app_entries, G_N_ELEMENTS (app_entries), self); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "app.paste", (const char *[]){ "v", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.fullscreen", (const char *[]){ "f", "F11", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.mute", (const char *[]){ "m", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.ff(+30000)", (const char *[]){ "Right", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.ff(-10000)", (const char *[]){ "Left", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.toggle-controls", (const char *[]){ "Escape", NULL }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.toggle-play", (const char *[]){"space", NULL, }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "win.open-file", (const char *[]){"o", NULL, }); gtk_application_set_accels_for_action (GTK_APPLICATION (self), "window.close", (const char *[]){ "q", NULL }); g_object_bind_property (window, "state", self->mpris, "player-state", G_BINDING_SYNC_CREATE); g_object_bind_property (window, "title", self->mpris, "title", G_BINDING_SYNC_CREATE); } static void on_url_processed (LiviUrlProcessor *url_processor, GAsyncResult *res, gpointer user_data) { LiviApplication *self = LIVI_APPLICATION (user_data); g_autoptr (GError) err = NULL; g_autofree char *url = NULL; g_assert (LIVI_IS_APPLICATION (self)); url = livi_url_processor_run_finish (url_processor, res, &err); if (!url) { GtkWindow *window; g_warning ("Failed to process url: %s", err->message); window = gtk_application_get_active_window (GTK_APPLICATION (self)); if (window) livi_window_set_error_state (LIVI_WINDOW (window), err->message); return; } g_debug ("Processed URL: %s", url); set_video_url (self, url); g_application_activate (G_APPLICATION (self)); } static int livi_application_command_line (GApplication *g_application, GApplicationCommandLine *cmdline) { LiviApplication *self = LIVI_APPLICATION (g_application); const gchar * const *remaining = NULL; g_autoptr (GFile) file = NULL; g_autoptr (GError) err = NULL; gboolean use_ytdlp = FALSE; g_autofree char *url = NULL; GVariantDict *options; gboolean demo, no_resume; int last = -1; gboolean success; success = g_application_register (G_APPLICATION (self), NULL, &err); if (!success) { g_warning ("Error registering: %s", err->message); return 1; } options = g_application_command_line_get_options_dict (cmdline); success = g_variant_dict_lookup (options, "h264-demo", "b", &demo); if (success) url = g_strdup (H264_DEMO_VIDEO); if (url == NULL) { success = g_variant_dict_lookup (options, "vp8-demo", "b", &demo); if (success) url = g_strdup (VP8_DEMO_VIDEO); } if (url == NULL) { success = g_variant_dict_lookup (options, G_OPTION_REMAINING, "^a&s", &remaining); if (success && remaining[0] != NULL) { file = g_file_new_for_commandline_arg (remaining[0]); url = g_file_get_uri (file); } } g_variant_dict_lookup (options, "no-resume", "b", &no_resume); self->resume = !no_resume; g_variant_dict_lookup (options, "yt-dlp", "b", &use_ytdlp); g_variant_dict_lookup (options, "last", "i", &last); if (last > 0) { g_autoptr (LiviRecentVideos) recent = livi_recent_videos_new (); gboolean preprocessed; url = livi_recent_videos_get_nth_recent_url (recent, last - 1, &preprocessed); if (url) { g_debug ("Resuming '%s', preprocessed: %d", url, preprocessed); if (preprocessed) use_ytdlp = preprocessed; } } if (url) { if (use_ytdlp) { /* The real video URL will be filled in when the url_processor finished */ set_video_urls (self, NULL, url); livi_url_processor_run (self->url_processor, url, NULL, (GAsyncReadyCallback)on_url_processed, self); } else { g_debug ("Video: %s", url); set_video_urls (self, url, NULL); } } g_application_activate (G_APPLICATION (self)); return 0; } static void livi_application_dispose (GObject *object) { LiviApplication *self = LIVI_APPLICATION (object); g_free (self->video_url); g_clear_object (&self->url_processor); g_clear_object (&self->mpris); G_OBJECT_CLASS (livi_application_parent_class)->dispose (object); } static void livi_application_class_init (LiviApplicationClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); GApplicationClass *application_class = G_APPLICATION_CLASS (klass); object_class->dispose = livi_application_dispose; application_class->startup = livi_application_startup; application_class->activate = livi_application_activate; application_class->command_line = livi_application_command_line; } const GOptionEntry options[] = { { "h264-demo", 0, 0, G_OPTION_ARG_NONE, NULL, "Play h264 demo", NULL }, { "vp8-demo", 0, 0, G_OPTION_ARG_NONE, NULL, "Play VP8 demo", NULL }, { "last", 0, 0, G_OPTION_ARG_INT, NULL, "Play nth most recently played video (1..N)", "number" }, { "no-resume", 0, 0, G_OPTION_ARG_NONE, NULL, "Skip resuming of videos", NULL }, { "yt-dlp", 'Y', 0, G_OPTION_ARG_NONE, NULL, "Let yt-dlp process the URL", NULL }, { G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, NULL, NULL, "[FILE]" }, { NULL,} }; static void livi_application_init (LiviApplication *self) { g_application_add_main_option_entries (G_APPLICATION (self), options); self->url_processor = livi_url_processor_new (); self->mpris = livi_mpris_new (); self->resume = TRUE; g_signal_connect_swapped (self->mpris, "raise", G_CALLBACK (on_mpris_raise), self); } LiviApplication * livi_application_new (void) { return g_object_new (LIVI_TYPE_APPLICATION, "application-id", APP_ID, "flags", G_APPLICATION_HANDLES_COMMAND_LINE, "register-session", TRUE, NULL); } gboolean livi_application_get_resume (LiviApplication *self) { g_assert (LIVI_IS_APPLICATION (self)); return self->resume; } livi-v0.1.0/src/livi-application.h000066400000000000000000000006761457505274000170670ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include G_BEGIN_DECLS #define LIVI_TYPE_APPLICATION (livi_application_get_type ()) G_DECLARE_FINAL_TYPE (LiviApplication, livi_application, LIVI, APPLICATION, AdwApplication) LiviApplication *livi_application_new (void); gboolean livi_application_get_resume (LiviApplication *self); G_END_DECLS livi-v0.1.0/src/livi-controls.c000066400000000000000000000166371457505274000164260ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-controls" #include "livi-config.h" #include "livi-controls.h" #include /** * LiviControls: * * The controls at the windows bottom */ enum { PROP_0, PROP_NARROW, LAST_PROP, }; static GParamSpec *props[LAST_PROP]; struct _LiviControls { AdwBin parent; GtkStack *stack; /* wide layout */ GtkAdjustment *wide_controls; GtkAdjustment *adj_duration; GtkMenuButton *btn_menu; GtkMenuButton *btn_lang_menu; GtkButton *btn_play; GtkImage *img_play; GtkButton *btn_mute; GtkImage *img_mute; GtkLabel *lbl_time; /* narrow layout */ GtkMenuButton *nrw_btn_menu; GtkMenuButton *nrw_btn_lang_menu; guint64 duration; guint64 position; GtkPopover *playback_menu; GtkPopoverMenu *lang_menu; gboolean narrow; }; G_DEFINE_TYPE (LiviControls, livi_controls, ADW_TYPE_BIN) static void set_narrow (LiviControls *self, gboolean narrow) { if (narrow == self->narrow) return; self->narrow = narrow; /* * Hide the wide layout when narrow as even a non visible stack page * adds up to the window size */ gtk_widget_set_visible (GTK_WIDGET (self->wide_controls), !narrow); gtk_stack_set_visible_child_name (self->stack, narrow ? "narrow" : "wide"); /* Switch over popovers as then can only have one parent */ if (narrow) { gtk_menu_button_set_popover (self->btn_menu, NULL); gtk_menu_button_set_popover (self->nrw_btn_menu, GTK_WIDGET (self->playback_menu)); gtk_menu_button_set_popover (self->btn_lang_menu, NULL); gtk_menu_button_set_popover (self->nrw_btn_lang_menu, GTK_WIDGET (self->lang_menu)); } else { gtk_menu_button_set_popover (self->nrw_btn_menu, NULL); gtk_menu_button_set_popover (self->btn_menu, GTK_WIDGET (self->playback_menu)); gtk_menu_button_set_popover (self->nrw_btn_lang_menu, NULL); gtk_menu_button_set_popover (self->btn_lang_menu, GTK_WIDGET (self->lang_menu)); } g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NARROW]); } static void livi_controls_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { LiviControls *self = LIVI_CONTROLS (object); switch (property_id) { case PROP_NARROW: set_narrow (self, g_value_get_boolean (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void livi_controls_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { LiviControls *self = LIVI_CONTROLS (object); switch (property_id) { case PROP_NARROW: g_value_set_boolean (value, self->narrow); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void update_slider (LiviControls *self, GstClockTime value) { g_autofree char *text = NULL; guint64 pos, dur; g_assert (LIVI_IS_CONTROLS (self)); gtk_adjustment_set_value (self->adj_duration, value); dur = self->duration / GST_SECOND; pos = value / GST_SECOND; text = g_strdup_printf ("%.2" G_GUINT64_FORMAT ":%.2" G_GUINT64_FORMAT " / %.2" G_GUINT64_FORMAT " :%.2" G_GUINT64_FORMAT, pos / 60, pos % 60, dur / 60, dur % 60); gtk_label_set_text (self->lbl_time, text); } static gboolean on_slider_value_changed (LiviControls *self, GtkScrollType scroll, double value) { /* Slider is ns, actions are ms */ value /= GST_MSECOND; if (value > G_MAXINT) value = G_MAXINT; gtk_widget_activate_action (GTK_WIDGET (self), "win.seek", "i", (int)value); return TRUE; } static void livi_controls_class_init (LiviControlsClass *klass) { GObjectClass *object_class = (GObjectClass *)klass; GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); object_class->get_property = livi_controls_get_property; object_class->set_property = livi_controls_set_property; props[PROP_NARROW] = g_param_spec_boolean ("narrow", "", "", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); gtk_widget_class_set_template_from_resource (widget_class, "/org/sigxcpu/Livi/livi-controls.ui"); gtk_widget_class_bind_template_child (widget_class, LiviControls, adj_duration); gtk_widget_class_bind_template_child (widget_class, LiviControls, btn_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, btn_mute); gtk_widget_class_bind_template_child (widget_class, LiviControls, btn_lang_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, btn_play); gtk_widget_class_bind_template_child (widget_class, LiviControls, img_mute); gtk_widget_class_bind_template_child (widget_class, LiviControls, img_play); gtk_widget_class_bind_template_child (widget_class, LiviControls, lang_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, lbl_time); gtk_widget_class_bind_template_child (widget_class, LiviControls, nrw_btn_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, nrw_btn_lang_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, playback_menu); gtk_widget_class_bind_template_child (widget_class, LiviControls, stack); gtk_widget_class_bind_template_child (widget_class, LiviControls, wide_controls); gtk_widget_class_bind_template_callback (widget_class, on_slider_value_changed); gtk_widget_class_set_css_name (widget_class, "livi-controls"); } static void livi_controls_init (LiviControls *self) { gtk_widget_init_template (GTK_WIDGET (self)); livi_controls_set_langs (self, NULL); } LiviControls * livi_controls_new (void) { return g_object_new (LIVI_TYPE_CONTROLS, NULL); } void livi_controls_set_duration (LiviControls *self, guint64 duration_ns) { g_assert (LIVI_IS_CONTROLS (self)); self->duration = duration_ns; gtk_adjustment_set_upper (self->adj_duration, self->duration); } void livi_controls_set_position (LiviControls *self, guint64 position_ns) { g_assert (LIVI_IS_CONTROLS (self)); update_slider(self, position_ns); } void livi_controls_show_mute_button (LiviControls *self, gboolean show) { g_assert (LIVI_IS_CONTROLS (self)); gtk_widget_set_visible (GTK_WIDGET (self->btn_mute), show); } void livi_controls_set_mute_icon (LiviControls *self, const char *icon_name) { g_assert (LIVI_IS_CONTROLS (self)); g_assert (icon_name); g_object_set (self->img_mute, "icon-name", icon_name, NULL); } void livi_controls_set_play_icon (LiviControls *self, const char *icon_name) { g_assert (LIVI_IS_CONTROLS (self)); g_assert (icon_name); g_object_set (self->img_play, "icon-name", icon_name, NULL); } void livi_controls_set_langs (LiviControls *self, GMenuModel *lang) { g_assert (LIVI_IS_CONTROLS (self)); g_assert (lang == NULL || G_IS_MENU_MODEL (lang)); gtk_popover_menu_set_menu_model (self->lang_menu, lang); gtk_widget_set_visible (GTK_WIDGET (self->btn_lang_menu), !!lang); } livi-v0.1.0/src/livi-controls.h000066400000000000000000000015211457505274000164150ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include G_BEGIN_DECLS #define LIVI_TYPE_CONTROLS (livi_controls_get_type ()) G_DECLARE_FINAL_TYPE (LiviControls, livi_controls, LIVI, CONTROLS, AdwBin) LiviControls *livi_controls_new (void); void livi_controls_set_duration (LiviControls *self, guint64 duration_ns); void livi_controls_set_position (LiviControls *self, guint64 position_ns); void livi_controls_show_mute_button (LiviControls *self, gboolean show); void livi_controls_set_mute_icon (LiviControls *self, const char *icon_name); void livi_controls_set_play_icon (LiviControls *self, const char *icon_name); void livi_controls_set_langs (LiviControls *self, GMenuModel *lang); G_END_DECLS livi-v0.1.0/src/livi-controls.ui000066400000000000000000000450031457505274000166060ustar00rootroot00000000000000 100 1 10 vertical 8 start Speed × 0.75 win.playback-speed 75 Normal win.playback-speed 100 speed-btns × 1.25 win.playback-speed 125 speed-btns × 1.5 win.playback-speed 175 speed-btns × 2 win.playback-speed 200 speed-btns livi-v0.1.0/src/livi-gst-paintable.c000066400000000000000000000217141457505274000173050ustar00rootroot00000000000000/* * SPDX-License-Identifier: LGPL-2.1-or-later * * Based on gtk-gst-paintable from GTK: * Copyright © 2018 Benjamin Otte * * Authors: Benjamin Otte * Guido Günther */ #include "livi-gst-paintable.h" #include "livi-gst-sink.h" #include #include #include #include struct _LiviGstPaintable { GObject parent_instance; GdkPaintable *image; double pixel_aspect_ratio; graphene_rect_t viewport; GdkGLContext *context; }; static void livi_gst_paintable_paintable_init (GdkPaintableInterface *iface); static void livi_gst_paintable_video_renderer_init (GstPlayVideoRendererInterface *iface); G_DEFINE_TYPE_WITH_CODE (LiviGstPaintable, livi_gst_paintable, G_TYPE_OBJECT, G_IMPLEMENT_INTERFACE (GDK_TYPE_PAINTABLE, livi_gst_paintable_paintable_init) G_IMPLEMENT_INTERFACE (GST_TYPE_PLAY_VIDEO_RENDERER, livi_gst_paintable_video_renderer_init)); static void livi_gst_paintable_paintable_snapshot (GdkPaintable *paintable, GdkSnapshot *snapshot, double width, double height) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (paintable); if (self->image) { float sx, sy; gtk_snapshot_save (snapshot); sx = gdk_paintable_get_intrinsic_width (self->image) / self->viewport.size.width; sy = gdk_paintable_get_intrinsic_height (self->image) / self->viewport.size.height; gtk_snapshot_push_clip (snapshot, &GRAPHENE_RECT_INIT (0, 0, width, height)); gtk_snapshot_translate (snapshot, &GRAPHENE_POINT_INIT (-self->viewport.origin.x * width / self->viewport.size.width, -self->viewport.origin.y * height / self->viewport.size.height)); gdk_paintable_snapshot (self->image, snapshot, width * sx, height * sy); gtk_snapshot_pop (snapshot); gtk_snapshot_restore (snapshot); } } static GdkPaintable * livi_gst_paintable_paintable_get_current_image (GdkPaintable *paintable) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (paintable); if (self->image) return GDK_PAINTABLE (g_object_ref (self->image)); return gdk_paintable_new_empty (0, 0); } static int livi_gst_paintable_paintable_get_intrinsic_width (GdkPaintable *paintable) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (paintable); if (self->image) return round (self->pixel_aspect_ratio * self->viewport.size.width); return 0; } static int livi_gst_paintable_paintable_get_intrinsic_height (GdkPaintable *paintable) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (paintable); if (self->image) return ceil (self->viewport.size.height); return 0; } static double livi_gst_paintable_paintable_get_intrinsic_aspect_ratio (GdkPaintable *paintable) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (paintable); if (self->image) return self->viewport.size.width / self->viewport.size.height; return 0.0; }; static void livi_gst_paintable_paintable_init (GdkPaintableInterface *iface) { iface->snapshot = livi_gst_paintable_paintable_snapshot; iface->get_current_image = livi_gst_paintable_paintable_get_current_image; iface->get_intrinsic_width = livi_gst_paintable_paintable_get_intrinsic_width; iface->get_intrinsic_height = livi_gst_paintable_paintable_get_intrinsic_height; iface->get_intrinsic_aspect_ratio = livi_gst_paintable_paintable_get_intrinsic_aspect_ratio; } static GstElement * livi_gst_paintable_video_renderer_create_video_sink (GstPlayVideoRenderer *renderer, GstPlay *player) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (renderer); GstElement *sink, *glsinkbin; g_autoptr (GdkGLContext) ctx = NULL; sink = g_object_new (LIVI_TYPE_GST_SINK, "paintable", self, "gl-context", self->context, NULL); g_object_get (LIVI_GST_SINK (sink), "gl-context", &ctx, NULL); /* GL rendering won't work otherwise */ g_assert (self->context != NULL && ctx != NULL); glsinkbin = gst_element_factory_make ("glsinkbin", NULL); g_object_set (glsinkbin, "sink", sink, NULL); g_debug ("created gl sink"); return glsinkbin; } static void livi_gst_paintable_video_renderer_init (GstPlayVideoRendererInterface *iface) { iface->create_video_sink = livi_gst_paintable_video_renderer_create_video_sink; } static void livi_gst_paintable_dispose (GObject *object) { LiviGstPaintable *self = LIVI_GST_PAINTABLE (object); g_clear_object (&self->image); G_OBJECT_CLASS (livi_gst_paintable_parent_class)->dispose (object); } static void livi_gst_paintable_class_init (LiviGstPaintableClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = livi_gst_paintable_dispose; } static void livi_gst_paintable_init (LiviGstPaintable *self) { } GdkPaintable * livi_gst_paintable_new (void) { return g_object_new (LIVI_TYPE_GST_PAINTABLE, NULL); } void livi_gst_paintable_realize (LiviGstPaintable *self, GdkSurface *surface) { g_autoptr (GError) error = NULL; if (self->context) return; self->context = gdk_surface_create_gl_context (surface, &error); if (self->context == NULL) { GST_INFO ("failed to create GDK GL context: %s", error->message); return; } if (!gdk_gl_context_realize (self->context, &error)) { GST_INFO ("failed to realize GDK GL context: %s", error->message); g_clear_object (&self->context); return; } } void livi_gst_paintable_unrealize (LiviGstPaintable *self, GdkSurface *surface) { /* XXX: We could be smarter here and: * - track how often we were realized with that surface * - track alternate surfaces */ if (self->context == NULL) return; if (gdk_gl_context_get_surface (self->context) == surface) g_clear_object (&self->context); } static void livi_gst_paintable_set_paintable (LiviGstPaintable *self, GdkPaintable *paintable, double pixel_aspect_ratio, const graphene_rect_t *viewport) { gboolean size_changed; if (self->image == paintable) return; if (self->image == NULL || gdk_paintable_get_intrinsic_height (self->image) != gdk_paintable_get_intrinsic_height (paintable) || !G_APPROX_VALUE (self->pixel_aspect_ratio * gdk_paintable_get_intrinsic_width (self->image), pixel_aspect_ratio * gdk_paintable_get_intrinsic_width (paintable), FLT_EPSILON) || !G_APPROX_VALUE (gdk_paintable_get_intrinsic_aspect_ratio (self->image), gdk_paintable_get_intrinsic_aspect_ratio (paintable), FLT_EPSILON) || !graphene_rect_equal (viewport, &self->viewport)) { size_changed = TRUE; } else { size_changed = FALSE; } g_set_object (&self->image, paintable); self->pixel_aspect_ratio = pixel_aspect_ratio; self->viewport = *viewport; if (size_changed) gdk_paintable_invalidate_size (GDK_PAINTABLE (self)); gdk_paintable_invalidate_contents (GDK_PAINTABLE (self)); } typedef struct _SetTextureInvocation { LiviGstPaintable *paintable; GdkTexture *texture; double pixel_aspect_ratio; graphene_rect_t viewport; } SetTextureInvocation; static void set_texture_invocation_free (SetTextureInvocation *invoke) { g_object_unref (invoke->paintable); g_object_unref (invoke->texture); g_slice_free (SetTextureInvocation, invoke); } static gboolean livi_gst_paintable_set_texture_invoke (gpointer data) { SetTextureInvocation *invoke = data; livi_gst_paintable_set_paintable (invoke->paintable, GDK_PAINTABLE (invoke->texture), invoke->pixel_aspect_ratio, &invoke->viewport); return G_SOURCE_REMOVE; } void livi_gst_paintable_queue_set_texture (LiviGstPaintable *self, GdkTexture *texture, double pixel_aspect_ratio, const graphene_rect_t *viewport) { SetTextureInvocation *invoke; invoke = g_slice_new0 (SetTextureInvocation); invoke->paintable = g_object_ref (self); invoke->texture = g_object_ref (texture); invoke->pixel_aspect_ratio = pixel_aspect_ratio; invoke->viewport = *viewport; g_main_context_invoke_full (NULL, G_PRIORITY_DEFAULT, livi_gst_paintable_set_texture_invoke, invoke, (GDestroyNotify) set_texture_invocation_free); } livi-v0.1.0/src/livi-gst-paintable.h000066400000000000000000000021631457505274000173070ustar00rootroot00000000000000/* * SPDX-License-Identifier: LGPL-2.1-or-later * * Authors: Benjamin Otte * Guido Günther * * Based on gtk-gst-paintable from GTK: * Copyright © 2018 Benjamin Otte */ #pragma once #include #include G_BEGIN_DECLS #define LIVI_TYPE_GST_PAINTABLE (livi_gst_paintable_get_type ()) G_DECLARE_FINAL_TYPE (LiviGstPaintable, livi_gst_paintable, LIVI, GST_PAINTABLE, GObject) GdkPaintable *livi_gst_paintable_new (void); void livi_gst_paintable_realize (LiviGstPaintable *self, GdkSurface *surface); void livi_gst_paintable_unrealize (LiviGstPaintable *self, GdkSurface *surface); void livi_gst_paintable_queue_set_texture (LiviGstPaintable *self, GdkTexture *texture, double pixel_aspect_ratio, const graphene_rect_t *viewport); G_END_DECLS livi-v0.1.0/src/livi-gst-sink.c000066400000000000000000000507531457505274000163170ustar00rootroot00000000000000/* * Copyright 2021 Purism SPC * * SPDX-License-Identifier: LGPL-2.1-or-later * * Authors: Matthew Waters * Benjamin Otte * Guido Günther * * heavily based on gtk-gst-sink.c from GTK4 which in turn picked it * up from GStreamer: * Copyright (C) 2015 Matthew Waters */ #include "livi-config.h" #include "livi-gst-sink.h" #include "livi-gst-paintable.h" #include #include #include #ifdef HAVE_GSTREAMER_DRM #include #include #endif enum { PROP_0, PROP_PAINTABLE, PROP_GL_CONTEXT, N_PROPS, }; struct _LiviGstSink { GstVideoSink parent; GstVideoInfo v_info; LiviGstPaintable *paintable; GdkGLContext *gdk_context; GstGLDisplay *gst_display; GstGLContext *gst_app_context; GstGLContext *gst_context; #ifdef HAVE_GSTREAMER_DRM GstVideoInfoDmaDrm drm_info; #endif }; GST_DEBUG_CATEGORY (livi_debug_gst_sink); #define GST_CAT_DEFAULT livi_debug_gst_sink #define FORMATS "{ BGRA, ARGB, RGBA, ABGR, RGB, BGR }" #define NOGL_CAPS GST_VIDEO_CAPS_MAKE (FORMATS) static GstStaticPadTemplate livi_gst_sink_template = GST_STATIC_PAD_TEMPLATE ("sink", GST_PAD_SINK, GST_PAD_ALWAYS, GST_STATIC_CAPS ( #ifdef HAVE_GSTREAMER_DRM GST_VIDEO_DMA_DRM_CAPS_MAKE "; " #endif "video/x-raw(" GST_CAPS_FEATURE_MEMORY_GL_MEMORY "), " "format = (string) RGBA, " "width = " GST_VIDEO_SIZE_RANGE ", " "height = " GST_VIDEO_SIZE_RANGE ", " "framerate = " GST_VIDEO_FPS_RANGE ", " "texture-target = (string) 2D" "; " NOGL_CAPS) ); G_DEFINE_TYPE_WITH_CODE (LiviGstSink, livi_gst_sink, GST_TYPE_VIDEO_SINK, GST_DEBUG_CATEGORY_INIT (livi_debug_gst_sink, "livigstsink", 0, "Livi Video Sink")); static GParamSpec *properties[N_PROPS] = { NULL, }; static void livi_gst_sink_get_times (GstBaseSink *bsink, GstBuffer *buf, GstClockTime *start, GstClockTime *end) { LiviGstSink *livi_sink; livi_sink = LIVI_GST_SINK (bsink); if (GST_BUFFER_TIMESTAMP_IS_VALID (buf)) { *start = GST_BUFFER_TIMESTAMP (buf); if (GST_BUFFER_DURATION_IS_VALID (buf)) *end = *start + GST_BUFFER_DURATION (buf); else { if (GST_VIDEO_INFO_FPS_N (&livi_sink->v_info) > 0) { *end = *start + gst_util_uint64_scale_int (GST_SECOND, GST_VIDEO_INFO_FPS_D (&livi_sink->v_info), GST_VIDEO_INFO_FPS_N (&livi_sink->v_info)); } } } } #ifdef HAVE_GSTREAMER_DRM static void add_drm_formats_and_modifiers (GstCaps *caps, GdkDmabufFormats *dmabuf_formats) { GValue dmabuf_list = G_VALUE_INIT; size_t i; g_value_init (&dmabuf_list, GST_TYPE_LIST); for (i = 0; i < gdk_dmabuf_formats_get_n_formats (dmabuf_formats); i++) { GValue value = G_VALUE_INIT; gchar *drm_format_string; guint32 fmt; guint64 mod; gdk_dmabuf_formats_get_format (dmabuf_formats, i, &fmt, &mod); if (mod == DRM_FORMAT_MOD_INVALID) continue; drm_format_string = gst_video_dma_drm_fourcc_to_string (fmt, mod); if (!drm_format_string) continue; g_value_init (&value, G_TYPE_STRING); g_value_take_string (&value, drm_format_string); gst_value_list_append_and_take_value (&dmabuf_list, &value); } gst_structure_take_value (gst_caps_get_structure (caps, 0), "drm-format", &dmabuf_list); } #endif static GstCaps * livi_gst_sink_get_caps (GstBaseSink *bsink, GstCaps *filter) { LiviGstSink *self = LIVI_GST_SINK (bsink); g_autoptr (GstCaps) tmp = NULL; GstCaps *result; if (self->gst_context) { tmp = gst_pad_get_pad_template_caps (GST_BASE_SINK_PAD (bsink)); #ifdef HAVE_GSTREAMER_DRM { GdkDisplay *display = gdk_gl_context_get_display (self->gdk_context); GdkDmabufFormats *formats = gdk_display_get_dmabuf_formats (display); tmp = gst_caps_make_writable (tmp); add_drm_formats_and_modifiers (tmp, formats); } #endif } else { tmp = gst_caps_from_string (NOGL_CAPS); } GST_DEBUG_OBJECT (self, "advertising own caps %" GST_PTR_FORMAT, tmp); if (filter) { GST_DEBUG_OBJECT (self, "intersecting with filter caps %" GST_PTR_FORMAT, filter); result = gst_caps_intersect_full (filter, tmp, GST_CAPS_INTERSECT_FIRST); } else { result = g_steal_pointer (&tmp); } GST_DEBUG_OBJECT (self, "returning caps: %" GST_PTR_FORMAT, result); return result; } static gboolean livi_gst_sink_set_caps (GstBaseSink *bsink, GstCaps *caps) { LiviGstSink *self = LIVI_GST_SINK (bsink); GST_DEBUG_OBJECT (self, "set caps with %" GST_PTR_FORMAT, caps); #ifdef HAVE_GSTREAMER_DRM if (gst_video_is_dma_drm_caps (caps)) { if (!gst_video_info_dma_drm_from_caps (&self->drm_info, caps)) return FALSE; if (!gst_video_info_dma_drm_to_video_info (&self->drm_info, &self->v_info)) return FALSE; } else { gst_video_info_dma_drm_init (&self->drm_info); #endif if (!gst_video_info_from_caps (&self->v_info, caps)) return FALSE; #ifdef HAVE_GSTREAMER_DRM } #endif return TRUE; } static gboolean livi_gst_sink_query (GstBaseSink *bsink, GstQuery *query) { LiviGstSink *self = LIVI_GST_SINK (bsink); if (GST_QUERY_TYPE (query) == GST_QUERY_CONTEXT && self->gst_display != NULL && gst_gl_handle_context_query (GST_ELEMENT (self), query, self->gst_display, self->gst_context, self->gst_app_context)) return TRUE; return GST_BASE_SINK_CLASS (livi_gst_sink_parent_class)->query (bsink, query); } static gboolean livi_gst_sink_propose_allocation (GstBaseSink *bsink, GstQuery *query) { LiviGstSink *self = LIVI_GST_SINK (bsink); g_autoptr (GstBufferPool) pool = NULL; GstStructure *config; GstCaps *caps; guint size; gboolean need_pool; GstVideoInfo info; if (!self->gst_context) return FALSE; gst_query_parse_allocation (query, &caps, &need_pool); if (caps == NULL) { GST_DEBUG_OBJECT (bsink, "no caps specified"); return FALSE; } #ifdef HAVE_GSTREAMER_DRM if (gst_caps_features_contains (gst_caps_get_features (caps, 0), GST_CAPS_FEATURE_MEMORY_DMABUF)) { gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0); return TRUE; } #endif if (!gst_caps_features_contains (gst_caps_get_features (caps, 0), GST_CAPS_FEATURE_MEMORY_GL_MEMORY)) return FALSE; if (!gst_video_info_from_caps (&info, caps)) { GST_DEBUG_OBJECT (self, "invalid caps specified"); return FALSE; } /* the normal size of a frame */ size = info.size; if (need_pool) { GST_DEBUG_OBJECT (self, "create new pool"); pool = gst_gl_buffer_pool_new (self->gst_context); config = gst_buffer_pool_get_config (pool); gst_buffer_pool_config_set_params (config, caps, size, 0, 0); gst_buffer_pool_config_add_option (config, GST_BUFFER_POOL_OPTION_GL_SYNC_META); if (!gst_buffer_pool_set_config (pool, config)) { GST_DEBUG_OBJECT (bsink, "failed setting config"); return FALSE; } } /* we need at least 2 buffer because we hold on to the last one */ gst_query_add_allocation_pool (query, pool, size, 2, 0); /* we also support various metadata */ gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, 0); if (self->gst_context->gl_vtable->FenceSync) gst_query_add_allocation_meta (query, GST_GL_SYNC_META_API_TYPE, 0); return TRUE; } static GdkMemoryFormat livi_gst_memory_format_from_video_info (GstVideoInfo *info) { switch ((guint) GST_VIDEO_INFO_FORMAT (info)) { case GST_VIDEO_FORMAT_BGRA: return GDK_MEMORY_B8G8R8A8; case GST_VIDEO_FORMAT_ARGB: return GDK_MEMORY_A8R8G8B8; case GST_VIDEO_FORMAT_RGBA: return GDK_MEMORY_R8G8B8A8; case GST_VIDEO_FORMAT_ABGR: return GDK_MEMORY_A8B8G8R8; case GST_VIDEO_FORMAT_RGB: return GDK_MEMORY_R8G8B8; case GST_VIDEO_FORMAT_BGR: return GDK_MEMORY_B8G8R8; default: g_assert_not_reached (); return GDK_MEMORY_A8R8G8B8; } } static void video_frame_free (GstVideoFrame *frame) { gst_video_frame_unmap (frame); g_free (frame); } static GdkTexture * livi_gst_sink_texture_from_buffer (LiviGstSink *self, GstBuffer *buffer, double *pixel_aspect_ratio, graphene_rect_t *viewport) { GstVideoFrame *frame = g_new (GstVideoFrame, 1); GdkTexture *texture; viewport->origin.x = 0; viewport->origin.y = 0; viewport->size.width = GST_VIDEO_INFO_WIDTH (&self->v_info); viewport->size.height = GST_VIDEO_INFO_HEIGHT (&self->v_info); #ifdef HAVE_GSTREAMER_DRM if (gst_is_dmabuf_memory (gst_buffer_peek_memory (buffer, 0))) { g_autoptr (GdkDmabufTextureBuilder) builder = NULL; const GstVideoMeta *vmeta = gst_buffer_get_video_meta (buffer); GError *error = NULL; int i; /* We don't map dmabufs */ g_clear_pointer (&frame, g_free); g_return_val_if_fail (vmeta, NULL); g_return_val_if_fail (self->gdk_context, NULL); g_return_val_if_fail (self->drm_info.drm_fourcc != DRM_FORMAT_INVALID, NULL); builder = gdk_dmabuf_texture_builder_new (); gdk_dmabuf_texture_builder_set_display (builder, gdk_gl_context_get_display (self->gdk_context)); gdk_dmabuf_texture_builder_set_fourcc (builder, self->drm_info.drm_fourcc); gdk_dmabuf_texture_builder_set_modifier (builder, self->drm_info.drm_modifier); gdk_dmabuf_texture_builder_set_width (builder, vmeta->width); gdk_dmabuf_texture_builder_set_height (builder, vmeta->height); gdk_dmabuf_texture_builder_set_n_planes (builder, vmeta->n_planes); for (i = 0; i < vmeta->n_planes; i++) { GstMemory *mem; guint mem_idx, length; gsize skip; if (!gst_buffer_find_memory (buffer, vmeta->offset[i], 1, &mem_idx, &length, &skip)) { GST_ERROR_OBJECT (self, "Buffer data is bogus"); return NULL; } mem = gst_buffer_peek_memory (buffer, mem_idx); gdk_dmabuf_texture_builder_set_fd (builder, i, gst_dmabuf_memory_get_fd (mem)); gdk_dmabuf_texture_builder_set_offset (builder, i, mem->offset + skip); gdk_dmabuf_texture_builder_set_stride (builder, i, vmeta->stride[i]); } texture = gdk_dmabuf_texture_builder_build (builder, (GDestroyNotify) gst_buffer_unref, gst_buffer_ref (buffer), &error); if (!texture) GST_ERROR_OBJECT (self, "Failed to create dmabuf texture: %s", error->message); *pixel_aspect_ratio = ((double) GST_VIDEO_INFO_PAR_N (&self->v_info) / (double) GST_VIDEO_INFO_PAR_D (&self->v_info)); } else #endif if (self->gdk_context && gst_video_frame_map (frame, &self->v_info, buffer, GST_MAP_READ | GST_MAP_GL)) { g_autoptr (GdkGLTextureBuilder) builder = NULL; GstGLSyncMeta *sync_meta; sync_meta = gst_buffer_get_gl_sync_meta (buffer); if (sync_meta) gst_gl_sync_meta_set_sync_point (sync_meta, self->gst_context); builder = gdk_gl_texture_builder_new (); gdk_gl_texture_builder_set_context (builder, self->gdk_context); gdk_gl_texture_builder_set_format (builder, livi_gst_memory_format_from_video_info (&frame->info)); gdk_gl_texture_builder_set_id (builder, *(guint *) frame->data[0]); gdk_gl_texture_builder_set_width (builder, frame->info.width); gdk_gl_texture_builder_set_height (builder, frame->info.height); gdk_gl_texture_builder_set_sync (builder, sync_meta ? sync_meta->data : NULL); texture = gdk_gl_texture_builder_build (builder, (GDestroyNotify) video_frame_free, frame); *pixel_aspect_ratio = ((double) frame->info.par_n) / ((double) frame->info.par_d); } else if (gst_video_frame_map (frame, &self->v_info, buffer, GST_MAP_READ)) { g_autoptr (GBytes) bytes = NULL; bytes = g_bytes_new_with_free_func (frame->data[0], frame->info.height * frame->info.stride[0], (GDestroyNotify) video_frame_free, frame); texture = gdk_memory_texture_new (frame->info.width, frame->info.height, livi_gst_memory_format_from_video_info (&frame->info), bytes, frame->info.stride[0]); *pixel_aspect_ratio = ((double) frame->info.par_n) / ((double) frame->info.par_d); } else { GST_ERROR_OBJECT (self, "Could not convert buffer to texture."); texture = NULL; g_free (frame); } return texture; } static GstFlowReturn livi_gst_sink_show_frame (GstVideoSink *vsink, GstBuffer *buf) { LiviGstSink *self; g_autoptr (GdkTexture) texture = NULL; double pixel_aspect_ratio; graphene_rect_t viewport; GST_TRACE ("rendering buffer:%p", buf); self = LIVI_GST_SINK (vsink); GST_OBJECT_LOCK (self); texture = livi_gst_sink_texture_from_buffer (self, buf, &pixel_aspect_ratio, &viewport); if (texture) livi_gst_paintable_queue_set_texture (self->paintable, texture, pixel_aspect_ratio, &viewport); GST_OBJECT_UNLOCK (self); return GST_FLOW_OK; } #define HANDLE_EXTERNAL_WGL_MAKE_CURRENT(ctx) #define DEACTIVATE_WGL_CONTEXT(ctx) #define REACTIVATE_WGL_CONTEXT(ctx) static gboolean livi_gst_sink_initialize_gl (LiviGstSink *self) { GdkDisplay *display; GError *error = NULL; GstGLPlatform platform = GST_GL_PLATFORM_NONE; GstGLAPI gl_api = GST_GL_API_NONE; guintptr gl_handle = 0; gboolean succeeded = FALSE; display = gdk_gl_context_get_display (self->gdk_context); HANDLE_EXTERNAL_WGL_MAKE_CURRENT (self->gdk_context); gdk_gl_context_make_current (self->gdk_context); if (GDK_IS_WAYLAND_DISPLAY (display)) { platform = GST_GL_PLATFORM_EGL; GST_DEBUG_OBJECT (self, "got EGL on Wayland!"); gl_api = gst_gl_context_get_current_gl_api (platform, NULL, NULL); gl_handle = gst_gl_context_get_current_gl_context (platform); if (gl_handle) { struct wl_display *wayland_display; wayland_display = gdk_wayland_display_get_wl_display (display); self->gst_display = GST_GL_DISPLAY (gst_gl_display_wayland_new_with_display (wayland_display)); self->gst_app_context = gst_gl_context_new_wrapped (self->gst_display, gl_handle, platform, gl_api); } else { GST_ERROR_OBJECT (self, "Failed to get handle from GdkGLContext, not using Wayland EGL"); return FALSE; } } else { GST_INFO_OBJECT (self, "Unsupported GDK display %s for GL", G_OBJECT_TYPE_NAME (display)); return FALSE; } g_assert (self->gst_app_context != NULL); gst_gl_context_activate (self->gst_app_context, TRUE); if (!gst_gl_context_fill_info (self->gst_app_context, &error)) { GST_ERROR_OBJECT (self, "failed to retrieve GDK context info: %s", error->message); g_clear_error (&error); g_clear_object (&self->gst_app_context); g_clear_object (&self->gst_display); HANDLE_EXTERNAL_WGL_MAKE_CURRENT (self->gdk_context); return FALSE; } else { DEACTIVATE_WGL_CONTEXT (self->gdk_context); gst_gl_context_activate (self->gst_app_context, FALSE); } succeeded = gst_gl_display_create_context (self->gst_display, self->gst_app_context, &self->gst_context, &error); if (!succeeded) { GST_ERROR_OBJECT (self, "Couldn't create GL context: %s", error->message); g_error_free (error); g_clear_object (&self->gst_app_context); g_clear_object (&self->gst_display); } HANDLE_EXTERNAL_WGL_MAKE_CURRENT (self->gdk_context); REACTIVATE_WGL_CONTEXT (self->gdk_context); return succeeded; } static void livi_gst_sink_set_property (GObject *object, guint prop_id, const GValue *value, GParamSpec *pspec) { LiviGstSink *self = LIVI_GST_SINK (object); switch (prop_id) { case PROP_PAINTABLE: self->paintable = g_value_dup_object (value); if (self->paintable == NULL) self->paintable = LIVI_GST_PAINTABLE (livi_gst_paintable_new ()); break; case PROP_GL_CONTEXT: self->gdk_context = g_value_dup_object (value); if (self->gdk_context != NULL && !livi_gst_sink_initialize_gl (self)) g_clear_object (&self->gdk_context); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void livi_gst_sink_get_property (GObject *object, guint prop_id, GValue *value, GParamSpec *pspec) { LiviGstSink *self = LIVI_GST_SINK (object); switch (prop_id) { case PROP_PAINTABLE: g_value_set_object (value, self->paintable); break; case PROP_GL_CONTEXT: g_value_set_object (value, self->gdk_context); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void livi_gst_sink_dispose (GObject *object) { LiviGstSink *self = LIVI_GST_SINK (object); g_clear_object (&self->paintable); g_clear_object (&self->gst_app_context); g_clear_object (&self->gst_display); g_clear_object (&self->gdk_context); G_OBJECT_CLASS (livi_gst_sink_parent_class)->dispose (object); } static void livi_gst_sink_class_init (LiviGstSinkClass * klass) { GObjectClass *gobject_class = G_OBJECT_CLASS (klass); GstElementClass *gstelement_class = GST_ELEMENT_CLASS (klass); GstBaseSinkClass *gstbasesink_class = GST_BASE_SINK_CLASS (klass); GstVideoSinkClass *gstvideosink_class = GST_VIDEO_SINK_CLASS (klass); gobject_class->set_property = livi_gst_sink_set_property; gobject_class->get_property = livi_gst_sink_get_property; gobject_class->dispose = livi_gst_sink_dispose; gstbasesink_class->set_caps = livi_gst_sink_set_caps; gstbasesink_class->get_times = livi_gst_sink_get_times; gstbasesink_class->query = livi_gst_sink_query; gstbasesink_class->propose_allocation = livi_gst_sink_propose_allocation; gstbasesink_class->get_caps = livi_gst_sink_get_caps; gstvideosink_class->show_frame = livi_gst_sink_show_frame; /** * LiviGstSink:paintable: * * The paintable that provides the picture for this sink. */ properties[PROP_PAINTABLE] = g_param_spec_object ("paintable", "paintable", "Paintable providing the picture", LIVI_TYPE_GST_PAINTABLE, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); /** * LiviGstSink:gl-context: * * The #GdkGLContext to use for GL rendering. */ properties[PROP_GL_CONTEXT] = g_param_spec_object ("gl-context", "gl-context", "GL context to use for rendering", GDK_TYPE_GL_CONTEXT, G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); g_object_class_install_properties (gobject_class, N_PROPS, properties); gst_element_class_set_metadata (gstelement_class, "Light Video gstreamer Sink", "Sink/Video", "The video sink used by Light Video", "Matthew Waters , " "Benjamin Otte "); gst_element_class_add_static_pad_template (gstelement_class, &livi_gst_sink_template); } static void livi_gst_sink_init (LiviGstSink *self) { } livi-v0.1.0/src/livi-gst-sink.h000066400000000000000000000006641457505274000163200ustar00rootroot00000000000000/* * Copyright 2021 Purism SPC * * SPDX-License-Identifier: LGPL-2.1-or-later */ #pragma once #include "livi-gst-paintable.h" #include #define GST_USE_UNSTABLE_API #include #include #include G_BEGIN_DECLS #define LIVI_TYPE_GST_SINK (livi_gst_sink_get_type ()) G_DECLARE_FINAL_TYPE (LiviGstSink, livi_gst_sink, LIVI, GST_SINK, GstVideoSink) G_END_DECLS livi-v0.1.0/src/livi-mpris.c000066400000000000000000000234141457505274000157040ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-mpris" #include "livi-config.h" #include "livi-mpris.h" #include "livi-mpris-dbus.h" #include #include #define MPRIS_BUS_NAME_PREFIX "org.mpris.MediaPlayer2." #define MPRIS_OBJECT_NAME "/org/mpris/MediaPlayer2" enum { RAISE, N_SIGNALS }; static guint signals[N_SIGNALS] = { 0 }; enum { PROP_0, PROP_PLAYER_STATE, PROP_TITLE, LAST_PROP, }; static GParamSpec *props[LAST_PROP]; struct _LiviMpris { GObject parent; int dbus_name_id; LiviDBusMediaPlayer2 *skeleton; LiviDBusMediaPlayer2Player *skeleton_player; GstPlayState state; char *title; }; G_DEFINE_TYPE (LiviMpris, livi_mpris, G_TYPE_OBJECT) static void livi_mpris_set_player_state (LiviMpris *self, GstPlayState state) { const char *dbus_state = "Stopped"; if (self->state == state) return; self->state = state; switch (self->state) { case GST_PLAY_STATE_PLAYING: dbus_state = "Playing"; break; case GST_PLAY_STATE_PAUSED: case GST_PLAY_STATE_BUFFERING: dbus_state = "Paused"; break; case GST_PLAY_STATE_STOPPED: default: break; } livi_dbus_media_player2_player_set_playback_status (self->skeleton_player, dbus_state); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PLAYER_STATE]); } static void livi_mpris_set_title (LiviMpris *self, const char *title) { GVariantDict dict; if (g_strcmp0 (self->title, title) == 0) return; g_free (self->title); self->title = g_strdup (title); g_variant_dict_init (&dict, NULL); g_variant_dict_insert (&dict, "xesam:title", "s", self->title); livi_dbus_media_player2_player_set_metadata (self->skeleton_player, g_variant_dict_end (&dict)); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); } static void livi_mpris_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { LiviMpris *self = LIVI_MPRIS (object); switch (property_id) { case PROP_PLAYER_STATE: livi_mpris_set_player_state (self, g_value_get_enum (value)); break; case PROP_TITLE: livi_mpris_set_title (self, g_value_get_string (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void livi_mpris_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { LiviMpris *self = LIVI_MPRIS (object); switch (property_id) { case PROP_PLAYER_STATE: g_value_set_enum (value, self->state); break; case PROP_TITLE: g_value_set_string (value, self->title); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static gboolean on_media_player_handle_raise (LiviMpris *self, GDBusMethodInvocation *invocation, LiviDBusMediaPlayer2 *skeleton) { g_debug ("Raise"); g_signal_emit (self, signals[RAISE], 0); livi_dbus_media_player2_complete_raise (skeleton, invocation); return TRUE; } static gboolean on_media_player_handle_play_pause (LiviMpris *self, GDBusMethodInvocation *invocation, LiviDBusMediaPlayer2Player *skeleton) { GApplication *app = g_application_get_default (); GtkWindow *window = gtk_application_get_active_window (GTK_APPLICATION (app)); g_debug ("Play-pause"); gtk_widget_activate_action (GTK_WIDGET (window), "win.toggle-play", NULL); livi_dbus_media_player2_player_complete_play_pause (skeleton, invocation); return TRUE; } static gboolean on_media_player_handle_seek (LiviMpris *self, GDBusMethodInvocation *invocation, gint64 pos_us, LiviDBusMediaPlayer2Player *skeleton) { GApplication *app = g_application_get_default (); GtkWindow *window = gtk_application_get_active_window (GTK_APPLICATION (app)); gint32 pos_ms; pos_ms = pos_us / 1000; if (pos_ms > G_MAXINT32) pos_ms = G_MAXINT32; if (pos_ms < G_MININT32) pos_ms = G_MININT32; g_debug ("Seek %" G_GINT32_MODIFIER "d", pos_ms); gtk_widget_activate_action (GTK_WIDGET (window), "win.ff", "i", pos_ms); livi_dbus_media_player2_player_complete_seek (skeleton, invocation); return TRUE; } static void on_name_acquired (GDBusConnection *connection, const char *name, gpointer user_data) { g_debug ("Acquired name %s", name); } static void on_name_lost (GDBusConnection *connection, const char *name, gpointer user_data) { g_debug ("Lost or failed to acquire name %s", name); } static void on_bus_acquired (GDBusConnection *connection, const char *name, gpointer user_data) { g_autoptr (GError) err = NULL; LiviMpris *self = LIVI_MPRIS (user_data); gboolean success; success = g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton), connection, MPRIS_OBJECT_NAME, &err); if (!success) { g_warning ("Failed to export mpris base: %s", err->message); g_clear_error (&err); } success = g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->skeleton_player), connection, MPRIS_OBJECT_NAME, &err); if (!success) g_warning ("Failed to export mpris player: %s", err->message); } static void livi_mpris_dispose (GObject *object) { LiviMpris *self = LIVI_MPRIS (object); livi_mpris_unexport (self); g_clear_object (&self->skeleton); g_clear_object (&self->skeleton_player); G_OBJECT_CLASS (livi_mpris_parent_class)->dispose (object); } static void livi_mpris_finalize (GObject *object) { LiviMpris *self = LIVI_MPRIS (object); g_clear_pointer (&self->title, g_free); G_OBJECT_CLASS (livi_mpris_parent_class)->finalize (object); } static void livi_mpris_class_init (LiviMprisClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->get_property = livi_mpris_get_property; object_class->set_property = livi_mpris_set_property; object_class->dispose = livi_mpris_dispose; object_class->finalize = livi_mpris_finalize; props[PROP_PLAYER_STATE] = g_param_spec_enum ("player-state", "", "", GST_TYPE_PLAY_STATE, GST_PLAY_STATE_STOPPED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_TITLE] = g_param_spec_string ("title", "", "", NULL, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); signals[RAISE] = g_signal_new ("raise", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 0); } static void livi_mpris_init (LiviMpris *self) { self->skeleton = livi_dbus_media_player2_skeleton_new (); self->skeleton_player = livi_dbus_media_player2_player_skeleton_new (); livi_dbus_media_player2_set_desktop_entry (self->skeleton, APP_ID); livi_dbus_media_player2_set_identity (self->skeleton, DISPLAY_NAME); livi_dbus_media_player2_set_can_raise (self->skeleton, TRUE); /* FIXME: need to unset this e.g. at end of play */ livi_dbus_media_player2_player_set_can_play (self->skeleton_player, TRUE); livi_dbus_media_player2_player_set_can_seek (self->skeleton_player, TRUE); livi_dbus_media_player2_player_set_playback_status (self->skeleton_player, "Stopped"); g_signal_connect_swapped (self->skeleton, "handle-raise", G_CALLBACK (on_media_player_handle_raise), self); g_object_connect (self->skeleton_player, "swapped-signal::handle-play-pause", on_media_player_handle_play_pause, self, "swapped-signal::handle-seek", on_media_player_handle_seek, self, NULL); } LiviMpris * livi_mpris_new (void) { return g_object_new (LIVI_TYPE_MPRIS, NULL); } void livi_mpris_export (LiviMpris *self) { g_assert (LIVI_IS_MPRIS (self)); if (self->dbus_name_id) return; self->dbus_name_id = g_bus_own_name (G_BUS_TYPE_SESSION, MPRIS_BUS_NAME_PREFIX PROJECT_NAME, G_BUS_NAME_OWNER_FLAGS_NONE, on_bus_acquired, on_name_acquired, on_name_lost, self, NULL); } void livi_mpris_unexport (LiviMpris *self) { g_assert (LIVI_IS_MPRIS (self)); if (!self->dbus_name_id) return; g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton)); g_dbus_interface_skeleton_unexport (G_DBUS_INTERFACE_SKELETON (self->skeleton_player)); g_clear_handle_id (&self->dbus_name_id, g_bus_unown_name); } livi-v0.1.0/src/livi-mpris.h000066400000000000000000000007101457505274000157030ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include #include G_BEGIN_DECLS #define LIVI_TYPE_MPRIS (livi_mpris_get_type ()) G_DECLARE_FINAL_TYPE (LiviMpris, livi_mpris, LIVI, MPRIS, GObject) LiviMpris *livi_mpris_new (void); void livi_mpris_export (LiviMpris *self); void livi_mpris_unexport (LiviMpris *self); G_END_DECLS livi-v0.1.0/src/livi-recent-videos.c000066400000000000000000000150701457505274000173200ustar00rootroot00000000000000/* * Copyright (C) 2024 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-recent-videos" #include "livi-config.h" #include "livi-recent-videos.h" #include #include /** * LiviRecentVideos: * * Tracks the list of recent videos. */ typedef struct _LiviRecentVideo { char *uri; gint32 pos_ms; guint64 lastseen_us; gboolean preprocessed; } LiviRecentVideo; struct _LiviRecentVideos { GObject parent; GSettings *settings; GHashTable *videos; }; G_DEFINE_TYPE (LiviRecentVideos, livi_recent_videos, G_TYPE_OBJECT) static void livi_recent_video_free (LiviRecentVideo *video) { g_free (video->uri); g_free (video); } static LiviRecentVideo * livi_recent_video_new (const char *uri, gint32 pos_ms, guint64 lastseen_us, gboolean preprocessed) { LiviRecentVideo *video = g_new0 (LiviRecentVideo, 1); video->uri = g_strdup (uri); video->pos_ms = pos_ms; video->lastseen_us = lastseen_us; video->preprocessed = preprocessed; return video; } static gint compare_recent_func (LiviRecentVideo *a, LiviRecentVideo *b) { return b->lastseen_us - a->lastseen_us; } static void serialize_videos (LiviRecentVideos *self) { g_autoptr (GPtrArray) videos = NULL; GVariantBuilder builder; guint max; max = g_settings_get_uint (self->settings, "max-recent-videos"); g_variant_builder_init (&builder, G_VARIANT_TYPE ("aa{sv}")); videos = g_hash_table_get_values_as_ptr_array (self->videos); g_ptr_array_sort_values (videos, (GCompareFunc)compare_recent_func); for (int i = 0; i < videos->len && i < max; i++) { LiviRecentVideo *video = g_ptr_array_index (videos, i); g_variant_builder_open (&builder, G_VARIANT_TYPE_VARDICT); g_variant_builder_add (&builder, "{sv}", "uri", g_variant_new_string (video->uri)); g_variant_builder_add (&builder, "{sv}", "position", g_variant_new_int32 (video->pos_ms)); g_variant_builder_add (&builder, "{sv}", "lastseen", g_variant_new_uint64 (video->lastseen_us)); g_variant_builder_add (&builder, "{sv}", "preprocessed", g_variant_new_boolean (video->preprocessed)); g_variant_builder_close (&builder); } g_settings_set_value (self->settings, "recent-videos", g_variant_builder_end (&builder)); } static void deserialize_videos (LiviRecentVideos *self) { GVariantIter videos_iter, dict_iter; g_autoptr (GVariant) videos = NULL; GVariant *video; g_hash_table_remove_all (self->videos); videos = g_settings_get_value (self->settings, "recent-videos"); g_variant_iter_init (&videos_iter, videos); while ((video = g_variant_iter_next_value (&videos_iter))) { LiviRecentVideo *v; g_autofree char *uri = NULL; gint32 pos_ms = 0; guint64 lastseen = 0; gboolean preprocessed = FALSE; const char *key; GVariant *value; g_variant_iter_init (&dict_iter, video); while (g_variant_iter_next (&dict_iter, "{&sv}", &key, &value)) { if (g_strcmp0 (key, "uri") == 0) uri = g_variant_dup_string (value, NULL); else if (g_strcmp0 (key, "position") == 0) pos_ms = g_variant_get_int32 (value); else if (g_strcmp0 (key, "lastseen") == 0) lastseen = g_variant_get_uint64 (value); else if (g_strcmp0 (key, "preprocessed") == 0) preprocessed = g_variant_get_boolean (value); g_variant_unref (value); } if (uri) { v = livi_recent_video_new (uri, pos_ms, lastseen, preprocessed); g_hash_table_insert (self->videos, g_steal_pointer (&uri), g_steal_pointer (&v)); } else { g_warning ("RecentVideos: got video but no uri"); } g_variant_unref (video); } } static void livi_recent_videos_dispose (GObject *object) { LiviRecentVideos *self = LIVI_RECENT_VIDEOS (object); g_clear_pointer (&self->videos, g_hash_table_destroy); g_clear_object (&self->settings); G_OBJECT_CLASS (livi_recent_videos_parent_class)->dispose (object); } static void livi_recent_videos_class_init (LiviRecentVideosClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->dispose = livi_recent_videos_dispose; } static void livi_recent_videos_init (LiviRecentVideos *self) { self->settings = g_settings_new ("org.sigxcpu.Livi"); self->videos = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, (GDestroyNotify)livi_recent_video_free); deserialize_videos (self); } LiviRecentVideos * livi_recent_videos_new (void) { return g_object_new (LIVI_TYPE_RECENT_VIDEOS, NULL); } void livi_recent_videos_update (LiviRecentVideos *self, const char *uri, gboolean preprocessed, guint64 position_ns) { LiviRecentVideo *video; gint32 pos_ms; g_assert (LIVI_IS_RECENT_VIDEOS (self)); g_assert (uri); g_debug ("Recent update '%s': %"G_GUINT64_FORMAT", preprocessed: %d", uri, position_ns / GST_SECOND, preprocessed); /* Don't overflow */ if (position_ns / GST_MSECOND > G_MAXINT32) { g_warning ("Stream pos too far: %"G_GUINT64_FORMAT, position_ns); pos_ms = 0; } else { pos_ms = position_ns / GST_MSECOND; } video = g_hash_table_lookup (self->videos, uri); if (video) { video->pos_ms = pos_ms; video->lastseen_us = g_get_real_time (); } else { video = livi_recent_video_new (uri, pos_ms, g_get_real_time (), preprocessed); g_hash_table_insert (self->videos, g_strdup (uri), video); } /* TODO: only on shutdown */ serialize_videos (self); } gint32 livi_recent_videos_get_pos (LiviRecentVideos *self, const char *uri) { LiviRecentVideo *video; g_assert (LIVI_IS_RECENT_VIDEOS (self)); g_assert (uri); video = g_hash_table_lookup (self->videos, uri); if (!video) { g_debug ("Video '%s' not yet known", uri); return 0; } g_debug ("Found position %ds for '%s'", video->pos_ms / 1000, uri); return video->pos_ms; } char * livi_recent_videos_get_nth_recent_url (LiviRecentVideos *self, guint index, gboolean *preprocessed) { g_autoptr (GPtrArray) videos = NULL; LiviRecentVideo *video; g_assert (LIVI_IS_RECENT_VIDEOS (self)); videos = g_hash_table_get_values_as_ptr_array (self->videos); g_ptr_array_sort_values (videos, (GCompareFunc)compare_recent_func); video = g_ptr_array_index (videos, index); if (!video) return NULL; if (preprocessed) *preprocessed = video->preprocessed; return g_strdup (video->uri); } livi-v0.1.0/src/livi-recent-videos.h000066400000000000000000000017721457505274000173310ustar00rootroot00000000000000/* * Copyright (C) 2024 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include G_BEGIN_DECLS #define LIVI_TYPE_RECENT_VIDEOS (livi_recent_videos_get_type ()) G_DECLARE_FINAL_TYPE (LiviRecentVideos, livi_recent_videos, LIVI, RECENT_VIDEOS, GObject) LiviRecentVideos *livi_recent_videos_new (void); void livi_recent_videos_update (LiviRecentVideos *self, const char *uri, gboolean preprocessed, guint64 position_ns); gint32 livi_recent_videos_get_pos (LiviRecentVideos *self, const char *uri); char *livi_recent_videos_get_nth_recent_url (LiviRecentVideos *self, guint index, gboolean *preprocessed); G_END_DECLS livi-v0.1.0/src/livi-url-processor.c000066400000000000000000000110101457505274000173560ustar00rootroot00000000000000/* * Copyright 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-url-processor" #include "livi-config.h" #include "livi-url-processor.h" #define URL_PROCESSOR "yt-dlp" /** * LiviUrlProcessor: * * Process an URL via yt-dlp so it can be streamed. */ struct _LiviUrlProcessor { GObject parent; GCancellable *cancel; char *name; }; G_DEFINE_TYPE (LiviUrlProcessor, livi_url_processor, G_TYPE_OBJECT); static void livi_url_processor_finalize (GObject *object) { LiviUrlProcessor *self = LIVI_URL_PROCESSOR(object); g_cancellable_cancel (self->cancel); g_clear_object (&self->cancel); G_OBJECT_CLASS (livi_url_processor_parent_class)->finalize (object); } static void livi_url_processor_class_init (LiviUrlProcessorClass *klass) { GObjectClass *object_class = G_OBJECT_CLASS (klass); object_class->finalize = livi_url_processor_finalize; } static void livi_url_processor_init (LiviUrlProcessor *self) { self->cancel = g_cancellable_new (); self->name = URL_PROCESSOR; } LiviUrlProcessor * livi_url_processor_new (void) { return g_object_new (LIVI_TYPE_URL_PROCESSOR, NULL); } const char * livi_url_processor_get_name (LiviUrlProcessor *self) { g_assert (LIVI_IS_URL_PROCESSOR (self)); return self->name; } static void on_url_processor_process_finish (GObject *source_object, GAsyncResult *res, gpointer user_data) { gboolean success; g_autoptr (GError) err = NULL; g_autoptr (GTask) task = G_TASK (user_data); char *new_url = NULL; GSubprocess *proc = G_SUBPROCESS (source_object); GInputStream *stdout, *stderr; g_autoptr (GDataInputStream) stdout_stream = NULL; g_autoptr (GDataInputStream) stderr_stream = NULL; success = g_subprocess_wait_finish (G_SUBPROCESS (source_object), res, &err); if (!success) { g_task_return_error (task, err); goto done; } stdout = g_subprocess_get_stdout_pipe (proc); if (!stdout) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Couldn't get stdout"); goto done; } stderr = g_subprocess_get_stderr_pipe (proc); if (!stderr) { g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "Couldn't get stderr"); goto done; } stdout_stream = g_data_input_stream_new (stdout); stderr_stream = g_data_input_stream_new (stderr); new_url = g_data_input_stream_read_line (stdout_stream, NULL, NULL, &err); if (!new_url) { if (err) { g_task_return_error (task, err); } else { g_autofree char *errmsg = g_data_input_stream_read_line (stderr_stream, NULL, NULL, &err); g_task_return_new_error (task, G_IO_ERROR, G_IO_ERROR_FAILED, "%s", errmsg); } goto done; } g_debug ("Got URL '%s'", new_url); g_task_return_pointer (task, new_url, g_free); done: g_object_unref (source_object); } void livi_url_processor_run (LiviUrlProcessor *self, const char *uri, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data) { g_autoptr (GSubprocess) proc = NULL; g_autoptr (GError) err = NULL; g_autoptr (GTask) task = NULL; task = g_task_new (self, cancellable, callback, user_data); g_task_set_name (task, "[livi] Url processor run"); g_debug ("Resolving '%s'", uri); proc = g_subprocess_new (G_SUBPROCESS_FLAGS_SEARCH_PATH_FROM_ENVP | G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE, &err, URL_PROCESSOR, "--get-url", uri, NULL); if (!proc) { g_warning ("Failed to find " URL_PROCESSOR ": %s", err->message); g_task_return_error (task, err); } g_task_set_source_tag (task, livi_url_processor_run); g_subprocess_wait_async (g_steal_pointer (&proc), cancellable, on_url_processor_process_finish, g_steal_pointer (&task)); } char * livi_url_processor_run_finish (LiviUrlProcessor *self, GAsyncResult *res, GError **error) { g_assert (LIVI_IS_URL_PROCESSOR (self)); g_assert (G_IS_TASK (res)); g_assert (!error || !*error); return g_task_propagate_pointer (G_TASK (res), error); } livi-v0.1.0/src/livi-url-processor.h000066400000000000000000000020751457505274000173760ustar00rootroot00000000000000/* * Copyright 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #pragma once #include #include G_BEGIN_DECLS #define LIVI_TYPE_URL_PROCESSOR (livi_url_processor_get_type ()) G_DECLARE_FINAL_TYPE (LiviUrlProcessor, livi_url_processor, LIVI, URL_PROCESSOR, GObject) LiviUrlProcessor *livi_url_processor_new (void); void livi_url_processor_run (LiviUrlProcessor *self, const char *uri, GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data); char *livi_url_processor_run_finish (LiviUrlProcessor *self, GAsyncResult *res, GError **error); const char *livi_url_processor_get_name (LiviUrlProcessor *self); G_END_DECLS livi-v0.1.0/src/livi-utils.h000066400000000000000000000003541457505274000157150ustar00rootroot00000000000000/* * Copyright (C) 2023 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later */ #pragma once #include G_BEGIN_DECLS #define STR_IS_NULL_OR_EMPTY(x) ((x) == NULL || (x)[0] == '\0') G_END_DECLS livi-v0.1.0/src/livi-window.c000066400000000000000000001104531457505274000160610ustar00rootroot00000000000000/* livi-window.c * * Copyright 2021 Purism SPC * 2023-2024 Guido Günther * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-window" #include "livi-config.h" #include "livi-application.h" #include "livi-controls.h" #include "livi-recent-videos.h" #include "livi-window.h" #include "livi-utils.h" #include "livi-gst-paintable.h" #include #include #include #include #include #include enum { PROP_0, PROP_MUTED, PROP_PLAYBACK_SPEED, PROP_STATE, PROP_TITLE, LAST_PROP, }; static GParamSpec *props[LAST_PROP]; typedef enum _StreamTargetState { STREAM_TARGET_STATE_NONE = 0, STREAM_TARGET_STATE_PREVIEW = 1, STREAM_TARGET_STATE_PLAY = 2, STREAM_TARGET_STATE_PAUSE = 3, } StreamTargetState; struct _LiviWindow { AdwApplicationWindow parent_instance; GtkStack *stack_content; GtkBox *box_content; GtkPicture *picture_video; GdkPaintable *paintable; GtkOverlay *overlay; AdwToolbarView *toolbar; /* top bar */ GtkLabel *lbl_status; GtkImage *img_accel; GtkImage *img_fullscreen; /* bottom bar */ LiviControls *controls; guint hide_controls_id; GtkRevealer *revealer_center; GtkStack *stack_center; GtkBox *box_center; GtkLabel *lbl_center; GtkImage *img_center; GtkBox *box_resume_or_restart; GtkButton *btn_resume; guint reveal_id; AdwStatusPage *error_state; GtkBox *empty_state; GstPlay *player; GstPlaySignalAdapter *signal_adapter; GstPlayState state; guint cookie; struct { gboolean muted; int playback_speed; guint num_audio_streams; guint num_subtitle_streams; char *title; char *ref_uri; gboolean uri_preprocessed; } stream; GtkFileFilter *video_filter; char *last_local_uri; /* seeking */ gboolean seek_lock; StreamTargetState seek_target_state; LiviRecentVideos *recent_videos; gboolean have_pointer; }; G_DEFINE_TYPE (LiviWindow, livi_window, ADW_TYPE_APPLICATION_WINDOW) static void hide_controls (LiviWindow *self) { adw_toolbar_view_set_reveal_bottom_bars (self->toolbar, FALSE); if (gtk_stack_get_visible_child (self->stack_content) == GTK_WIDGET (self->box_content)) adw_toolbar_view_set_reveal_top_bars (self->toolbar, FALSE); gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "none"); } static void on_hide_controls_timeout (gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); if (self->state == GST_PLAY_STATE_PLAYING || self->seek_target_state == STREAM_TARGET_STATE_PAUSE) { hide_controls (self); } self->hide_controls_id = 0; } static void arm_hide_controls_timer (LiviWindow *self) { g_clear_handle_id (&self->hide_controls_id, g_source_remove); self->hide_controls_id = g_timeout_add_seconds_once (2, on_hide_controls_timeout, self); g_source_set_name_by_id (self->hide_controls_id, "[p-o-s] hide_controls_timer"); } static void show_controls (LiviWindow *self) { adw_toolbar_view_set_reveal_top_bars (self->toolbar, TRUE); if (gtk_stack_get_visible_child (self->stack_content) == GTK_WIDGET (self->box_content)) adw_toolbar_view_set_reveal_bottom_bars (self->toolbar, TRUE); if (self->have_pointer) arm_hide_controls_timer (self); gtk_widget_set_cursor_from_name (GTK_WIDGET (self), "default"); } static void on_pointer_enter (LiviWindow *self) { g_clear_handle_id (&self->hide_controls_id, g_source_remove); } static void on_pointer_motion (LiviWindow *self, double x, double y) { static double old_x, old_y; /* Avoid busy work when nothing changed */ if (G_APPROX_VALUE (old_x, x, FLT_EPSILON) && G_APPROX_VALUE (old_y, y, FLT_EPSILON)) { return; } old_x = x; old_y = y; self->have_pointer = TRUE; show_controls (self); } static void reset_stream (LiviWindow *self) { if (self->stream.title) { g_free (self->stream.title); g_free (self->stream.ref_uri); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); } memset (&self->stream, 0, sizeof (self->stream)); self->stream.playback_speed = 100; } static void livi_window_set_playback_speed (LiviWindow *self, int percent) { g_debug ("Setting Rate to : %f", percent / 100.0); if (percent == self->stream.playback_speed) return; if (self->player) gst_play_set_rate (self->player, percent / 100.0); self->stream.playback_speed = percent; g_object_notify_by_pspec (G_OBJECT (self), props[PROP_PLAYBACK_SPEED]); } static void livi_window_set_property (GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { LiviWindow *self = LIVI_WINDOW (object); gboolean muted; switch (property_id) { case PROP_MUTED: muted = g_value_get_boolean (value); if (self->player) gst_play_set_mute (self->player, muted); else self->stream.muted = muted; break; case PROP_PLAYBACK_SPEED: livi_window_set_playback_speed (self, g_value_get_int (value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void livi_window_get_property (GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { LiviWindow *self = LIVI_WINDOW (object); switch (property_id) { case PROP_MUTED: g_value_set_boolean (value, self->stream.muted); break; case PROP_PLAYBACK_SPEED: g_value_set_int (value, self->stream.playback_speed); break; case PROP_STATE: g_value_set_enum (value, self->state); break; case PROP_TITLE: g_value_set_string (value, self->stream.title); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec); break; } } static void on_fullscreen (LiviWindow *self) { const char *icon_names[] = { "view-fullscreen-symbolic", "view-unfullscreen-symbolic" }; gboolean fullscreen; g_assert (LIVI_IS_WINDOW (self)); fullscreen = gtk_window_is_fullscreen (GTK_WINDOW (self)); g_debug ("Fullscreen: %d", fullscreen); g_object_set (self->img_fullscreen, "icon-name", icon_names[fullscreen], NULL); } static void on_is_active_changed (LiviWindow *self) { g_assert (LIVI_IS_WINDOW (self)); if (!gtk_window_is_active (GTK_WINDOW (self))) return; show_controls (self); } static void on_reveal_timeout (gpointer data) { LiviWindow *self = LIVI_WINDOW (data); gtk_revealer_set_reveal_child (self->revealer_center, FALSE); self->reveal_id = 0; } static void show_center_overlay (LiviWindow *self, const char *icon_name, const char *label, gboolean fade) { gtk_widget_set_visible (GTK_WIDGET (self->lbl_center), !!label); gtk_label_set_label (self->lbl_center, label); gtk_stack_set_visible_child (self->stack_center, GTK_WIDGET (self->box_center)); g_object_set (self->img_center, "icon-name", icon_name, NULL); gtk_revealer_set_reveal_child (self->revealer_center, TRUE); if (fade) self->reveal_id = g_timeout_add_once (500, on_reveal_timeout, self); } static void hide_center_overlay (LiviWindow *self) { if (self->seek_lock) return; gtk_revealer_set_reveal_child (self->revealer_center, FALSE); } static void show_resume_or_restart_overlay (LiviWindow *self, gboolean can_resume) { gtk_widget_set_visible (GTK_WIDGET (self->btn_resume), can_resume); gtk_stack_set_visible_child (self->stack_center, GTK_WIDGET (self->box_resume_or_restart)); gtk_revealer_set_reveal_child (self->revealer_center, TRUE); self->seek_target_state = STREAM_TARGET_STATE_PREVIEW; } static void toggle_controls (LiviWindow *self) { gboolean revealed; g_assert (LIVI_IS_WINDOW (self)); revealed = adw_toolbar_view_get_reveal_bottom_bars (self->toolbar); if (!revealed) show_controls (self); else hide_controls (self); } static void on_toggle_controls_activated (GtkWidget *widget, const char *action_name, GVariant *unused) { toggle_controls (LIVI_WINDOW (widget)); } static void on_toggle_play_activated (GtkWidget *widget, const char *action_name, GVariant *unused) { LiviWindow *self = LIVI_WINDOW (widget); const char *icon_name; gboolean fade; if (self->state == GST_PLAY_STATE_PLAYING) { gst_play_pause (self->player); icon_name = "media-playback-pause-symbolic"; fade = FALSE; } else { gst_play_play (self->player); icon_name = "media-playback-start-symbolic"; fade = TRUE; } show_center_overlay (self, icon_name, NULL, fade); gtk_stack_set_visible_child (self->stack_content, GTK_WIDGET (self->box_content)); } static void on_restart_activated (GtkWidget *widget, const char *action_name, GVariant *unused) { LiviWindow *self = LIVI_WINDOW (widget); self->seek_target_state = STREAM_TARGET_STATE_PLAY; gst_play_seek (self->player, 0); } static void on_subtitle_stream_action_changed_state (GSimpleAction *action, GVariant *param, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); gboolean enable = FALSE; gint index; index = g_variant_get_int32 (param); if (index >= 0) { gst_play_set_subtitle_track (self->player, index); g_debug ("Enabling subtitle track %d", index); enable = TRUE; } gst_play_set_subtitle_track_enabled (self->player, enable); g_simple_action_set_state(action, param); } static void on_audio_stream_action_changed_state (GSimpleAction *action, GVariant *param, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); gint index; index = g_variant_get_int32 (param); gst_play_set_audio_track (self->player, index); g_simple_action_set_state(action, param); } static void on_file_chooser_done (GObject *object, GAsyncResult *response, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); g_autoptr (GtkFileDialog) dialog = GTK_FILE_DIALOG (object); g_autoptr (GFile) file = NULL; g_autoptr (GError) err = NULL; g_autofree char *uri = NULL; file = gtk_file_dialog_open_finish (dialog, response, &err); if (!file) { if (!g_error_matches (err, GTK_DIALOG_ERROR, GTK_DIALOG_ERROR_DISMISSED)) g_warning ("Failed to select file: %s", err->message); return; } uri = g_file_get_uri (file); livi_window_play_uri (self, uri, NULL); g_free (self->last_local_uri); self->last_local_uri = g_steal_pointer (&uri); } static void on_open_file_activated (GtkWidget *widget, const char *action_name, GVariant *unused) { LiviWindow *self = LIVI_WINDOW (widget); GtkFileDialog *dialog; /* Otherwise the portal dialog can set this as proper parent */ gtk_window_unfullscreen (GTK_WINDOW (self)); dialog = gtk_file_dialog_new (); gtk_file_dialog_set_title (dialog, _("Choose a video to play")); gtk_file_dialog_set_default_filter (dialog, self->video_filter); if (!STR_IS_NULL_OR_EMPTY (self->last_local_uri)) { g_autoptr (GFile) current_file = g_file_new_for_uri (self->last_local_uri); gtk_file_dialog_set_initial_file (dialog, current_file); } else { const char *dir; dir = g_get_user_special_dir (G_USER_DIRECTORY_VIDEOS); if (dir) { g_autoptr (GFile) videos_dir = g_file_new_for_path (dir); gtk_file_dialog_set_initial_folder (dialog, videos_dir); } } gtk_file_dialog_open (dialog, GTK_WINDOW (self), NULL, on_file_chooser_done, self); } static void move_stream_to_pos (LiviWindow *self, GstClockTime pos, const char *label) { GstClockTime current; const char *icon_name; current = gst_play_get_position (self->player); if (pos == current) return; icon_name = (pos > current) ? "media-seek-forward-symbolic" : "media-seek-backward-symbolic"; show_center_overlay (self, icon_name, label, TRUE); gst_play_seek (self->player, pos); } static void on_ff_rev_activated (GtkWidget *widget, const char *action_name, GVariant *param) { LiviWindow *self = LIVI_WINDOW (widget); GstClockTime pos; g_autofree char *label = NULL; gint64 offset; if (self->seek_lock) return; self->seek_lock = TRUE; offset = g_variant_get_int32 (param) * GST_MSECOND; pos = gst_play_get_position (self->player); if (offset < 0 && labs(offset) > pos) pos = 0; else pos += offset; label = g_strdup_printf (_("%.2lds"), labs(offset / GST_SECOND)); move_stream_to_pos (self, pos, label); } static void on_seek_activated (GtkWidget *widget, const char *action_name, GVariant *param) { LiviWindow *self = LIVI_WINDOW (widget); gint64 pos; if (self->seek_lock) return; self->seek_lock = TRUE; pos = g_variant_get_int32 (param) * GST_MSECOND; move_stream_to_pos (self, pos, NULL); } static void on_player_error (GstPlaySignalAdapter *adapter, GError *error, GstStructure *details, LiviWindow *self) { g_warning ("Player error: %s", error->message); livi_window_set_error_state (self, error->message); } static void on_player_warning (GstPlaySignalAdapter *adapter, GError *error, GstStructure *details, LiviWindow *self) { g_warning ("Player warning: %s", error->message); } static void on_player_buffering (GstPlaySignalAdapter *adapter, gint percent, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); g_autofree char *msg = NULL; g_assert (LIVI_IS_WINDOW (self)); g_debug ("Buffering %d", percent); if (percent == 100) { gtk_widget_set_visible (GTK_WIDGET (self->lbl_status), FALSE); return; } msg = g_strdup_printf (_("Buffering %d/100"), percent); gtk_label_set_text (self->lbl_status, msg); gtk_widget_set_visible (GTK_WIDGET (self->lbl_status), TRUE); } static void check_pipeline (LiviWindow *self, GstPlay *player) { g_autoptr (GstElement) bin = gst_play_get_pipeline (player); g_autoptr (GstIterator) iter = gst_bin_iterate_recurse (GST_BIN (bin)); GValue item = { 0, }; gboolean found = FALSE; while (iter && gst_iterator_next (iter, &item) == GST_ITERATOR_OK) { GstElement *elem = g_value_get_object (&item); if (g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slav1dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slh264dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slh265dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slmpeg2dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slvp8dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2slvp9dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2h264dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2h265dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2mpeg2dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2vp8dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "v4l2vp9dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vaav1dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vah264dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vah265dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vampeg2dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vavp8dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vavp9dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vulkanav1dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vulkanh264dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vulkanh265dec") || g_str_has_prefix (GST_OBJECT_NAME (elem), "vulkanvp9dec")) { found = TRUE; g_value_unset (&item); break; } g_value_unset (&item); } if (!found) g_warning ("Hardware accelerated video decoding not in use, playback will likely be slow"); gtk_widget_set_visible (GTK_WIDGET (self->img_accel), !found); } static void on_player_state_changed (GstPlaySignalAdapter *adapter, GstPlayState state, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); GApplication *app = g_application_get_default (); const char *icon; g_assert (LIVI_IS_WINDOW (self)); g_debug ("State %s", gst_play_state_get_name (state)); self->state = state; if (state == GST_PLAY_STATE_PLAYING) { icon = "media-playback-pause-symbolic"; self->cookie = gtk_application_inhibit (GTK_APPLICATION (app), GTK_WINDOW (self), GTK_APPLICATION_INHIBIT_SUSPEND | GTK_APPLICATION_INHIBIT_IDLE, "Playing video"); check_pipeline (self, self->player); if (self->seek_target_state == STREAM_TARGET_STATE_PREVIEW) { /* The stream was only started to have a preview picture */ self->seek_target_state = STREAM_TARGET_STATE_NONE; livi_window_set_pause (self); } else { hide_center_overlay (self); if (self->have_pointer) arm_hide_controls_timer (self); } } else { icon = "media-playback-start-symbolic"; if (self->cookie) { gtk_application_uninhibit (GTK_APPLICATION (app), self->cookie); self->cookie = 0; } } /* Switch to desired target state after seek */ switch (self->seek_target_state) { case STREAM_TARGET_STATE_PLAY: self->seek_target_state = STREAM_TARGET_STATE_NONE; gst_play_play (self->player); break; case STREAM_TARGET_STATE_PAUSE: self->seek_target_state = STREAM_TARGET_STATE_NONE; gst_play_pause (self->player); break; case STREAM_TARGET_STATE_PREVIEW: case STREAM_TARGET_STATE_NONE: break; default: g_assert_not_reached (); } livi_controls_set_play_icon (self->controls, icon); livi_recent_videos_update (self->recent_videos, self->stream.ref_uri, self->stream.uri_preprocessed, gst_play_get_position (self->player)); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); } static void on_player_mute_changed (GstPlaySignalAdapter *adapter, gboolean muted, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); const char *icon; g_assert (LIVI_IS_WINDOW (self)); if (self->stream.muted == muted) return; self->stream.muted = muted; g_debug ("Muted %d", muted); icon = muted ? "audio-volume-muted-symbolic" : "audio-volume-medium-symbolic"; livi_controls_set_mute_icon (self->controls, icon); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_MUTED]); } static void on_player_duration_changed (GstPlaySignalAdapter *adapter, guint64 duration, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); g_assert (LIVI_IS_WINDOW (self)); g_debug ("Duration %" G_GUINT64_FORMAT "s", duration / GST_SECOND); livi_controls_set_duration (self->controls, duration); } static void on_player_position_updated (GstPlaySignalAdapter *adapter, guint64 position, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); self->seek_lock = FALSE; g_assert (LIVI_IS_WINDOW (self)); livi_controls_set_position (self->controls, position); } static gboolean is_sdh (GstPlayStreamInfo *si) { GstTagList *tags; g_autofree char *title = NULL; tags = gst_play_stream_info_get_tags (si); if (tags == NULL) return FALSE; if (!gst_tag_list_get_string (tags, GST_TAG_TITLE, &title)) return FALSE; /* This is likely incomplete */ if (strstr (title, "(SDH)") || strstr (title, "[SDH]")) return TRUE; return FALSE; } G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstPlayAudioInfo, g_object_unref) G_DEFINE_AUTOPTR_CLEANUP_FUNC (GstPlaySubtitleInfo, g_object_unref) static void update_audio_streams (LiviWindow *self, GstPlayMediaInfo *info) { g_autoptr (GMenu) menu = NULL; g_autoptr (GMenu) lang_section = NULL; g_autoptr (GMenu) subtitles_section = NULL; guint num_audio_streams, num_subtitle_streams; GList *streams; num_audio_streams = gst_play_media_info_get_number_of_audio_streams (info); num_subtitle_streams = gst_play_media_info_get_number_of_subtitle_streams (info); if (num_audio_streams == self->stream.num_audio_streams && num_subtitle_streams == self->stream.num_subtitle_streams) { return; } self->stream.num_audio_streams = num_audio_streams; self->stream.num_subtitle_streams = num_subtitle_streams; if (num_audio_streams >= 2) { g_autoptr (GstPlayAudioInfo) current = NULL; GAction *sa; gint index = -1; current = gst_play_get_current_audio_track (self->player); if (current) index = gst_play_stream_info_get_index (GST_PLAY_STREAM_INFO (current)); /* Ensure the current subtitle is pre-selected in the popover */ sa = g_action_map_lookup_action (G_ACTION_MAP (self), "audio-stream"); g_simple_action_set_state (G_SIMPLE_ACTION (sa), g_variant_new_int32 (index)); streams = gst_play_media_info_get_audio_streams (info); lang_section = g_menu_new (); for (GList *l = streams; l; l = l->next) { GstPlayAudioInfo *ai = GST_PLAY_AUDIO_INFO (l->data); g_autofree char *action = NULL; const char *lang; index = gst_play_stream_info_get_index (GST_PLAY_STREAM_INFO (ai)); if (index < 0) continue; lang = gst_play_audio_info_get_language (ai); action = g_strdup_printf ("win.audio-stream(%d)", index); g_menu_insert (lang_section, -1, lang, action); } } if (num_subtitle_streams) { g_autoptr (GstPlaySubtitleInfo) current = NULL; GAction *sa; gint index = -1; current = gst_play_get_current_subtitle_track (self->player); if (current) index = gst_play_stream_info_get_index (GST_PLAY_STREAM_INFO (current)); /* Ensure the current subtitle is pre-selected in the popover */ sa = g_action_map_lookup_action (G_ACTION_MAP (self), "subtitle-stream"); g_simple_action_set_state (G_SIMPLE_ACTION (sa), g_variant_new_int32 (index)); streams = gst_play_media_info_get_subtitle_streams (info); subtitles_section = g_menu_new (); /* Translators: None here means: disable subtitles */ g_menu_insert (subtitles_section, -1, _("None"), "win.subtitle-stream(-1)"); for (GList *l = streams; l; l = l->next) { GstPlaySubtitleInfo *si = GST_PLAY_SUBTITLE_INFO (l->data); g_autofree char *action = NULL; g_autofree char *label = NULL; index = gst_play_stream_info_get_index (GST_PLAY_STREAM_INFO (si)); if (index < 0) continue; if (is_sdh (GST_PLAY_STREAM_INFO (si))) { /* translators: SDH in this context means deaf/hard of hearing */ label = g_strdup_printf (_("%s (SDH)"), gst_play_subtitle_info_get_language (si)); } else { label = g_strdup (gst_play_subtitle_info_get_language (si)); } action = g_strdup_printf ("win.subtitle-stream(%d)", index); g_menu_insert (subtitles_section, -1, label, action); } } if (!lang_section && !subtitles_section) { livi_controls_set_langs (self->controls, NULL); return; } menu = g_menu_new (); if (lang_section) g_menu_insert_section (menu, -1, _("Languages"), G_MENU_MODEL (lang_section)); if (subtitles_section) g_menu_insert_section (menu, -1, _("Subtitles"), G_MENU_MODEL (subtitles_section)); livi_controls_set_langs (self->controls, G_MENU_MODEL (menu)); } static void update_title (LiviWindow *self, GstPlayMediaInfo *info) { g_autofree char *title = NULL; title = g_strdup (gst_play_media_info_get_title (info)); if (!title) { const char *uri; g_autofree char *filename = NULL; uri = gst_play_media_info_get_uri (info); filename = g_filename_from_uri (uri, NULL, NULL); if (filename) title = g_path_get_basename (filename); else title = g_strdup (self->stream.ref_uri); } if (g_strcmp0 (title, self->stream.title) != 0) { g_free (self->stream.title); self->stream.title = g_steal_pointer (&title); g_object_notify_by_pspec (G_OBJECT (self), props[PROP_TITLE]); } } static void update_video_streams (LiviWindow *self, GstPlayMediaInfo *info) { guint num_video_streams; GstPlayVisualization **vis = NULL; gboolean success; const char *visname = NULL; num_video_streams = gst_play_media_info_get_number_of_video_streams (info); if (num_video_streams) { gst_play_set_visualization_enabled (self->player, FALSE); return; } vis = gst_play_visualizations_get (); if (!vis[0]) { g_warning ("No visualizations"); goto out; } for (int i = 0; vis[i]; i++) { if (g_strcmp0 (vis[i]->name, "wavescope")) visname = "wavescope"; } if (!visname) visname = vis[0]->name; success = gst_play_set_visualization (self->player, visname); if (!success) { g_warning ("Failed to enable visualization %s", visname); goto out; } g_debug ("Enabling visuzlization: %s", visname); gst_play_set_visualization_enabled (self->player, TRUE); out: gst_play_visualizations_free (vis); } static void on_media_info_updated (GstPlaySignalAdapter *adapter, GstPlayMediaInfo *info, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); gboolean show; show = gst_play_media_info_get_number_of_audio_streams (info); update_audio_streams (self, info); update_video_streams (self, info); update_title (self, info); livi_controls_show_mute_button (self->controls, !!show); } static void on_end_of_stream (GstPlaySignalAdapter *adapter, gpointer user_data) { LiviWindow *self = LIVI_WINDOW (user_data); g_debug ("End of stream"); show_resume_or_restart_overlay (self, FALSE); } static void on_realize (LiviWindow *self) { GdkSurface *surface; g_assert (LIVI_IS_WINDOW (self)); surface = gtk_native_get_surface (gtk_widget_get_native (GTK_WIDGET (self->picture_video))); livi_gst_paintable_realize (LIVI_GST_PAINTABLE (self->paintable), surface); if (!self->player) { self->player = gst_play_new (GST_PLAY_VIDEO_RENDERER (g_object_ref (self->paintable))); self->signal_adapter = gst_play_signal_adapter_new (self->player); g_object_connect (self->signal_adapter, "signal::error", G_CALLBACK (on_player_error), self, "signal::warning", G_CALLBACK (on_player_warning), self, "signal::buffering", G_CALLBACK (on_player_buffering), self, "signal::state-changed", G_CALLBACK (on_player_state_changed), self, "signal::mute-changed", G_CALLBACK (on_player_mute_changed), self, "signal::duration-changed", G_CALLBACK (on_player_duration_changed), self, "signal::position-updated", G_CALLBACK (on_player_position_updated), self, "signal::media-info-updated", G_CALLBACK (on_media_info_updated), self, "signal::end-of-stream", G_CALLBACK (on_end_of_stream), self, NULL); } } static gboolean livi_window_close_request (GtkWindow *window) { LiviWindow *self = LIVI_WINDOW (window); if (self->stream.ref_uri) { livi_recent_videos_update (self->recent_videos, self->stream.ref_uri, self->stream.uri_preprocessed, gst_play_get_position (self->player)); } return GTK_WINDOW_CLASS (livi_window_parent_class)->close_request (window); } static void livi_window_dispose (GObject *obj) { LiviWindow *self = LIVI_WINDOW (obj); g_clear_pointer (&self->last_local_uri, g_free); g_clear_object (&self->recent_videos); g_clear_object (&self->signal_adapter); g_clear_object (&self->player); if (self->cookie) { GApplication *app = g_application_get_default (); gtk_application_uninhibit (GTK_APPLICATION (app), self->cookie); self->cookie = 0; } reset_stream (self); G_OBJECT_CLASS (livi_window_parent_class)->dispose (obj); } static void livi_window_class_init (LiviWindowClass *klass) { GObjectClass *object_class = (GObjectClass *)klass; GtkWidgetClass *widget_class = GTK_WIDGET_CLASS (klass); GtkWindowClass *window_class = GTK_WINDOW_CLASS (klass); GtkCssProvider *provider; AdwStyleManager *manager = adw_style_manager_get_default (); object_class->get_property = livi_window_get_property; object_class->set_property = livi_window_set_property; object_class->dispose = livi_window_dispose; window_class->close_request = livi_window_close_request; props[PROP_MUTED] = g_param_spec_boolean ("muted", "", "", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_PLAYBACK_SPEED] = g_param_spec_int ("playback-speed", "", "", 10, G_MAXINT, 100, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_STATE] = g_param_spec_enum ("state", "", "", GST_TYPE_PLAY_STATE, GST_PLAY_STATE_STOPPED, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); props[PROP_TITLE] = g_param_spec_string ("title", "", "", NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_EXPLICIT_NOTIFY); g_object_class_install_properties (object_class, LAST_PROP, props); g_type_ensure (LIVI_TYPE_CONTROLS); gtk_widget_class_set_template_from_resource (widget_class, "/org/sigxcpu/Livi/livi-window.ui"); gtk_widget_class_bind_template_child (widget_class, LiviWindow, box_content); gtk_widget_class_bind_template_child (widget_class, LiviWindow, box_center); gtk_widget_class_bind_template_child (widget_class, LiviWindow, box_resume_or_restart); gtk_widget_class_bind_template_child (widget_class, LiviWindow, btn_resume); gtk_widget_class_bind_template_child (widget_class, LiviWindow, controls); gtk_widget_class_bind_template_child (widget_class, LiviWindow, empty_state); gtk_widget_class_bind_template_child (widget_class, LiviWindow, error_state); gtk_widget_class_bind_template_child (widget_class, LiviWindow, img_fullscreen); gtk_widget_class_bind_template_child (widget_class, LiviWindow, img_accel); gtk_widget_class_bind_template_child (widget_class, LiviWindow, img_center); gtk_widget_class_bind_template_child (widget_class, LiviWindow, lbl_center); gtk_widget_class_bind_template_child (widget_class, LiviWindow, lbl_status); gtk_widget_class_bind_template_child (widget_class, LiviWindow, overlay); gtk_widget_class_bind_template_child (widget_class, LiviWindow, picture_video); gtk_widget_class_bind_template_child (widget_class, LiviWindow, revealer_center); gtk_widget_class_bind_template_child (widget_class, LiviWindow, stack_center); gtk_widget_class_bind_template_child (widget_class, LiviWindow, stack_content); gtk_widget_class_bind_template_child (widget_class, LiviWindow, toolbar); gtk_widget_class_bind_template_child (widget_class, LiviWindow, video_filter); gtk_widget_class_bind_template_callback (widget_class, on_fullscreen); gtk_widget_class_bind_template_callback (widget_class, on_is_active_changed); gtk_widget_class_bind_template_callback (widget_class, on_pointer_motion); gtk_widget_class_bind_template_callback (widget_class, on_pointer_enter); gtk_widget_class_bind_template_callback (widget_class, on_realize); gtk_widget_class_install_property_action (widget_class, "win.fullscreen", "fullscreened"); gtk_widget_class_install_property_action (widget_class, "win.mute", "muted"); gtk_widget_class_install_property_action (widget_class, "win.playback-speed", "playback-speed"); gtk_widget_class_install_action (widget_class, "win.toggle-controls", NULL, on_toggle_controls_activated); gtk_widget_class_install_action (widget_class, "win.ff", "i", on_ff_rev_activated); gtk_widget_class_install_action (widget_class, "win.seek", "i", on_seek_activated); gtk_widget_class_install_action (widget_class, "win.toggle-play", NULL, on_toggle_play_activated); gtk_widget_class_install_action (widget_class, "win.open-file", NULL, on_open_file_activated); gtk_widget_class_install_action (widget_class, "win.restart", NULL, on_restart_activated); provider = gtk_css_provider_new (); gtk_css_provider_load_from_resource (provider, "/org/sigxcpu/Livi/style.css"); gtk_style_context_add_provider_for_display (gdk_display_get_default (), GTK_STYLE_PROVIDER (provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); adw_style_manager_set_color_scheme (manager, ADW_COLOR_SCHEME_PREFER_DARK); } static void add_controls_toggle (LiviWindow *self, GtkWidget *widget) { GtkGesture *gesture = gtk_gesture_click_new (); g_signal_connect_swapped (gesture, "pressed", G_CALLBACK (toggle_controls), self); gtk_event_controller_set_propagation_phase (GTK_EVENT_CONTROLLER (gesture), GTK_PHASE_TARGET); gtk_widget_add_controller (widget, GTK_EVENT_CONTROLLER (gesture)); } static GActionEntry win_entries[] = { { "subtitle-stream", NULL, "i", "-1", on_subtitle_stream_action_changed_state }, { "audio-stream", NULL, "i", "0", on_audio_stream_action_changed_state }, }; static void livi_window_init (LiviWindow *self) { reset_stream (self); gtk_widget_init_template (GTK_WIDGET (self)); self->paintable = livi_gst_paintable_new (); gtk_picture_set_paintable (self->picture_video, self->paintable); add_controls_toggle (self, GTK_WIDGET (self->picture_video)); add_controls_toggle (self, GTK_WIDGET (self->revealer_center)); arm_hide_controls_timer (self); self->recent_videos = livi_recent_videos_new (); g_action_map_add_action_entries (G_ACTION_MAP (self), win_entries, G_N_ELEMENTS (win_entries), self); } static void livi_window_resume_pos (LiviWindow *self) { gint64 pos; LiviApplication *app = LIVI_APPLICATION (g_application_get_default ()); if (!livi_application_get_resume (app)) return; pos = livi_recent_videos_get_pos (self->recent_videos, self->stream.ref_uri); if (pos > 0) { pos *= GST_MSECOND; /* Seek directly without showing any overlays */ g_debug ("Found video %s, resuming at %ld s", self->stream.ref_uri, pos / GST_SECOND); gst_play_seek (self->player, pos); show_resume_or_restart_overlay (self, TRUE); } } static void livi_window_set_uris (LiviWindow *self, const char *uri, const char *ref_uri) { g_assert (LIVI_IS_WINDOW (self)); reset_stream (self); gtk_stack_set_visible_child (self->stack_content, GTK_WIDGET (self->box_content)); gst_play_set_uri (self->player, uri); if (ref_uri) { self->stream.ref_uri = g_strdup (ref_uri); self->stream.uri_preprocessed = TRUE; } else { self->stream.ref_uri = g_strdup (uri); self->stream.uri_preprocessed = FALSE; } livi_window_resume_pos (self); } void livi_window_set_empty_state (LiviWindow *self) { g_assert (LIVI_IS_WINDOW (self)); hide_controls (self); gtk_stack_set_visible_child (self->stack_content, GTK_WIDGET (self->empty_state)); } void livi_window_set_error_state (LiviWindow *self, const char *description) { g_assert (LIVI_IS_WINDOW (self)); hide_controls (self); gtk_stack_set_visible_child (self->stack_content, GTK_WIDGET (self->error_state)); adw_status_page_set_description (self->error_state, description); } void livi_window_set_play (LiviWindow *self) { g_assert (LIVI_IS_WINDOW (self)); gst_play_play (self->player); } void livi_window_set_pause (LiviWindow *self) { g_assert (LIVI_IS_WINDOW (self)); gst_play_pause (self->player); } /** * livi_window_play_uri: * @self: the window * @uri: The uri to play * @ref_uri:(nullable): The reference uri * * Plays the given URL. if `ref_url` is given that is used instead of the "real" * URL when e.g. remembering player state. This can be usedful for preprocessed * URLs that give the "backend" URL that changes between plays. * * If `ref_uri` is `NULL` it's assumed to be identical to the `uri`. */ void livi_window_play_uri (LiviWindow *self, const char *uri, const char *ref_uri) { g_assert (LIVI_IS_WINDOW (self)); g_debug ("Playing %s %s", uri, ref_uri); livi_window_set_uris (self, uri, ref_uri); livi_window_set_play (self); arm_hide_controls_timer (self); } livi-v0.1.0/src/livi-window.h000066400000000000000000000012201457505274000160550ustar00rootroot00000000000000/* livi-window.h * * Copyright 2021 Purism SPC * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #pragma once #include G_BEGIN_DECLS #define LIVI_TYPE_WINDOW (livi_window_get_type()) G_DECLARE_FINAL_TYPE (LiviWindow, livi_window, LIVI, WINDOW, AdwApplicationWindow) void livi_window_set_empty_state (LiviWindow *self); void livi_window_set_error_state (LiviWindow *self, const char *description); void livi_window_set_play (LiviWindow *self); void livi_window_set_pause (LiviWindow *self); void livi_window_play_uri (LiviWindow *self, const char *uri, const char *ref_uri); G_END_DECLS livi-v0.1.0/src/livi-window.ui000066400000000000000000000345511457505274000162600ustar00rootroot00000000000000 horizontal 100 1 10
Open file… win.open-file
Keyboard Shortcuts win.show-help-overlay About Livi app.about
Videos video/mpeg-system video/msvideo> video/ogg webm mp4 mkv
livi-v0.1.0/src/livi.gresource.xml000066400000000000000000000024501457505274000171240ustar00rootroot00000000000000 livi-window.ui livi-controls.ui style.css gtk/help-overlay.ui ../data/org.sigxcpu.Livi.metainfo.xml ../data/icons/play-large-symbolic.svg ../data/icons/view-restore-symbolic.svg ../data/icons/skip-backwards-10-symbolic.svg ../data/icons/skip-forward-30-symbolic.svg ../data/icons/speedometer4-symbolic.svg ../data/icons/region-symbolic.svg livi-v0.1.0/src/main.c000066400000000000000000000056551457505274000145440ustar00rootroot00000000000000/* main.c * * Copyright 2021 Purism SPC * * SPDX-License-Identifier: GPL-3.0-or-later * * Author: Guido Günther */ #define G_LOG_DOMAIN "livi-main" #include "livi-config.h" #include "livi-application.h" #include "livi-url-processor.h" #include "livi-window.h" #include #include #include #include static void on_screensaver_active_changed (GtkApplication *app) { gboolean active; g_object_get (G_OBJECT (app), "screensaver-active", &active, NULL); if (active) { GtkWindow *window = gtk_application_get_active_window (app); if (window) { g_debug ("Screensaver active, pausing player"); livi_window_set_pause (LIVI_WINDOW (window)); } } } static gboolean fix_broken_cache (void) { const gchar *name; g_autoptr (GstElementFactory) factory = NULL; if (!g_getenv ("FLATPAK_SANDBOX_DIR")) return TRUE; factory = gst_element_factory_find ("playbin"); name = gst_element_factory_get_metadata (factory, GST_ELEMENT_METADATA_LONGNAME); g_debug ("playbin plugin is %s", gst_element_factory_get_metadata (factory, GST_ELEMENT_METADATA_LONGNAME)); /* If playbin is playbin 3 in the registry drop the cache, rebuilding is not enough */ if (g_strcmp0 (name, "Player Bin 3") == 0) { const char * const arches[] = {"x86_64", "aarch64", NULL}; g_warning ("Found playbin3 as playbin, this will cause problems. Removing cache"); gst_deinit (); for (int i = 0; i < g_strv_length ((GStrv)arches); i++) { g_autofree char *path = NULL; path = g_strdup_printf (".var/app/" APP_ID "/cache/gstreamer-1.0/registry.%s.bin", arches[i]); if (unlink(path) == 0) g_debug ("Unlinked %s", path); } return FALSE; } return TRUE; } static gboolean on_shutdown_signal (gpointer user_data) { GActionGroup *app = G_ACTION_GROUP (user_data); GtkWindow *window; g_assert (LIVI_IS_APPLICATION (app)); window = gtk_application_get_active_window (GTK_APPLICATION (app)); if (window) gtk_widget_activate_action (GTK_WIDGET (window), "window.close", NULL); return G_SOURCE_REMOVE; } int main (int argc, char *argv[]) { g_autoptr (LiviApplication) app = NULL; /* TODO: Until we configure the full pipeline */ g_setenv ("GST_PLAY_USE_PLAYBIN3", "1", TRUE); /* This causes all kinds of trouble since it swaps playbin3 in without without gst-play knowing */ g_unsetenv ("USE_PLAYBIN3"); bindtextdomain (GETTEXT_PACKAGE, LOCALEDIR); bind_textdomain_codeset (GETTEXT_PACKAGE, "UTF-8"); textdomain (GETTEXT_PACKAGE); gst_init (&argc, &argv); if (!fix_broken_cache ()) return 1; app = livi_application_new (); g_signal_connect (app, "notify::screensaver-active", G_CALLBACK (on_screensaver_active_changed), NULL); g_unix_signal_add (SIGINT, on_shutdown_signal, app); g_unix_signal_add (SIGTERM, on_shutdown_signal, app); return g_application_run (G_APPLICATION (app), argc, argv); } livi-v0.1.0/src/meson.build000066400000000000000000000027241457505274000156100ustar00rootroot00000000000000subdir('dbus') livi_sources = [ 'main.c', 'livi-application.c', 'livi-controls.c', 'livi-mpris.c', 'livi-window.c', 'livi-recent-videos.c', 'livi-gst-paintable.c', 'livi-gst-sink.c', 'livi-url-processor.c', ] + generated_dbus_sources gst_ver = '>= 1.22' gst_allocators_dep = dependency('gstreamer-allocators-1.0', version: gst_ver) dmabuf_passthrough = false if not get_option('dmabuf-passthrough').disabled() if gst_allocators_dep.version().version_compare('>= 1.23.1') dmabuf_passthrough = true endif endif config_h.set('HAVE_GSTREAMER_DRM', dmabuf_passthrough) gtk4_dep = dependency( 'gtk4', version: '>= 4.13.7', fallback: ['gtk4', 'gtk_dep'], default_options: [ 'introspection=disabled', 'documentation=false', 'gtk_doc=false', 'build-demos=false', 'build-testsuite=false', 'build-examples=false', 'build-tests=false', ], ) livi_deps = [ dependency('gio-2.0', version: '>= 2.50'), dependency('gstreamer-1.0', version: gst_ver), gst_allocators_dep, dependency('gstreamer-gl-1.0', version: gst_ver), dependency('gstreamer-play-1.0', version: gst_ver), dependency('libadwaita-1', version: '>= 1.4'), gtk4_dep, cc.find_library('m', required: false), ] livi_sources += gnome.compile_resources('livi-resources', 'livi.gresource.xml', c_name: 'livi', dependencies: appstream_file, source_dir: meson.current_build_dir()) executable('livi', livi_sources, dependencies: livi_deps, install: true, ) livi-v0.1.0/src/style.css000066400000000000000000000012771457505274000153220ustar00rootroot00000000000000@define-color livi_control_bg_color alpha(@window_bg_color, 0.9); headerbar { background: none; } headerbar button { padding: 6px; background-color: @livi_control_bg_color; } windowcontrols button.close { background: @livi_control_bg_color; border-radius: 9999px; -gtk-icon-size: 16px; } windowcontrols button.close image { background: none; } .livi-window { background-color: black; } .livi-controls { background-color: @livi_control_bg_color; font-feature-settings: "tnum"; } .livi-controls.wide { margin: 16px; border-radius: 6px; } .livi-error { background-color: rgba(80, 80, 80, 0.8); color: @warning_color; margin: 4px 16px 4px 16px; border-radius: 4px; } livi-v0.1.0/subprojects/000077500000000000000000000000001457505274000152155ustar00rootroot00000000000000livi-v0.1.0/subprojects/gtk4.wrap000066400000000000000000000001321457505274000167550ustar00rootroot00000000000000[wrap-git] directory=gtk url=https://gitlab.gnome.org/GNOME/gtk.git revision=main depth=1