ClutterGst Rotation

今時のスタンドアロンな動画プレイヤーには回転機能が必須だ。
なんたって回転編集していないスマホ動画を結構見かけるようになった。
昨年まではスマホ動画が主体になるなんて思いもしなかったのに。

ということで、ClutterGst で映像の回転を行う関数を探してみよう。

Clutter Gst 3.0.24 Reference Manual: Clutter Gst 3.0.24 Reference Manual

無いんカイ!

生 Gst を直で使うなら手段があった気がするんだがこれは困った。
いやまて、ClutterActor 自体を回転させればいいんでないの?
そのための OpenGL ES じゃないか、ということで実験コード。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const Lang = imports.lang;
const Clutter = imports.gi.Clutter;
const ClutterGst = imports.gi.ClutterGst;
const GtkClutter = imports.gi.GtkClutter;

ClutterGst.init(null);

// width:640 height:800
const PATH = "/home/sasakima-nao/movie/GF/動く苗ちゃん(1).mp4";

const RotateWindow = new Lang.Class({
    Name: 'RotateWindow',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // player
        this.player = new ClutterGst.Playback();
        this.player.set_filename(PATH);
        this.content = new ClutterGst.Content();
        this.content.set_player(this.player);
        this.actor = new Clutter.Actor();
        this.actor.set_content(this.content);
        // embed
        let embed = new GtkClutter.Embed();
        embed.get_stage().add_child(this.actor);
        this.add(embed);
        // size
        this.resize(450, 450);
        this.actor.set_width(320);
        this.actor.set_height(400);
        this.actor.set_position(65, 25);
        // rotation
        this.actor.set_pivot_point(0.5, 0.5);
        this.actor.set_rotation_angle(Clutter.RotateAxis.Z_AXIS, 90);
        //
        this.show_all();
        this.player.set_playing(true);
    }
});

let app = new Gtk.Application();
app.connect("activate", function() {
    new RotateWindow(app);
});
app.run(null);

ピボットポイントを中心にして Z 軸で回せば映像も普通に回転するね。
リサイズ時には 90/270 度の時に縦横の値を入れ替えるだけでイケそうだ。
もっと正しい手段があるかもだけど筆者はコレでいいや。

あぁやっと完全放置だった Y901x を更新するネタができたぞい。
だって Youtube でオッサンが田舎道をバイクで走っているだけのばかり見ていたし。
ドラマやアニメより個人制作な素人動画のほうが妙に面白いよね。

LOKDocView

本日の Fedora アップデートでこんなのが出た。

LOKDocView って何だ?

Development/Integrating LOKDocView and GNOME Documents – The Document Foundation Wiki

libreoffice 文書を GNOME Document で表示する API なのか。
ということは gir でバインドされているのかな。

普通にあったわ、コレって今まであったっけ?
まあそれはいいや、さてサンプルコードを探してみよう。

GitHub – pranavk/lokdocviewer: An application for testing LOKDocView

js/py 両方用意してくれているとは親切ですね。
しかし new メソッドの引数が PyGObject は 2 つで Gjs は 3 つ。
Gjs らしく new キーワードに書き換えるとコアダンプ。

#!/usr/bin/gjs

const Gtk = imports.gi.Gtk;
const LOKDocView = imports.gi.LOKDocView;
const Lang = imports.lang;

const LOKWindow = new Lang.Class({
    Name: 'LOKWindow',
    Extends: Gtk.ApplicationWindow,

    _init: function(app) {
        this.parent({
            application: app
        });
        // Segmentation fault
        //this.view = new LOKDocView.View();
        this.view = LOKDocView.View.new(null, null, null); // OK
        this.view.open_document(
            "/home/sasakima-nao/syokumu.odt",
            "{}",
            null,
            Lang.bind(this, function() {
                this.view.set_edit(true);
            }),
            null);
        let sw = new Gtk.ScrolledWindow();
        sw.add(this.view);
        this.add(sw);
        //
        this.show_all();
    }
});

let app = new Gtk.Application();
app.connect("activate", function() {
    new LOKWindow(app);
});
app.run(null);

前々回みたいな問題があるし…
GNOME はぶっちゃけ Gjs に全面移行したいのだろうけどまだ問題が多いなぁ。
てか Python はサードパーティなのに対応っぷりがスゲェ。
当面は Python と併用が続くのだろう。

コレを使って何か作るかな、ここんとこネタ切れなのは秘密だよ。

Koiking

久々に面白いスマホゲーム発見。
『はねろ!コイキング』公式サイト

どうせまた数日で飽きると思っていたけど続いている。
ただポケ GO みたく現在時刻も表示してほしいな、休憩時間にちょっと育成って時に困る。

検索したらガチな人が結構いてビックリ。
ポケ GO のおかげでキャラがある程度解る人が多いのが大きいのかも。
筆者はガンダム世代だしポケ GO が出なかったらポケモンなんて知らないまま。
ゲームはやはりキャラだよ、豪華声優はゲンナリするだけ。

Linux プログラミングに飽きてきたのでスマホアプリとか考えたけど…
ゲームって内容よりキャラが重要なんだよな、うーん。

ClutterImage PyGObject/Gjs

おまたせ、Comipoli Gjs 版が遅くて出せない原因が判明しました。

#!/usr/bin/env python3

import gi
gi.require_version('Clutter', '1.0')
gi.require_version('GdkPixbuf', '2.0')
from gi.repository import Clutter, Cogl, GdkPixbuf

Clutter.init()

PICTURE = "burgman400.jpg"

pixbuf = GdkPixbuf.Pixbuf.new_from_file(PICTURE)
image = Clutter.Image()
image.set_data(
    pixbuf.get_pixels(),
    Cogl.PixelFormat.RGB_888,
    pixbuf.get_width(),
    pixbuf.get_height(),
    pixbuf.get_rowstride()
)

#!/usr/bin/gjs

const Clutter = imports.gi.Clutter;
const Cogl = imports.gi.Cogl;
const GdkPixbuf = imports.gi.GdkPixbuf;

Clutter.init(null);

const PICTURE = "burgman400.jpg";

let pixbuf = GdkPixbuf.Pixbuf.new_from_file(PICTURE);
let image = new Clutter.Image();
image.set_data(
    pixbuf.get_pixels(),
    Cogl.PixelFormat.RGB_888,
    pixbuf.get_width(),
    pixbuf.get_height(),
    pixbuf.get_rowstride()
);

何ですかこの圧倒的なスピード差は!!!
というより Gjs のこの異様な遅さは何なんだ?
GdkPixbuf を使うだけなら特に差が無いのに。

憶測だけど PyGObject は get_pixels でバイナリ出力を直接使っていると思う。
Gjs は多分バイナリ出力を Uint8Array オブジェクトに変換している。
言語仕様の制限だろうからセット関数の追加が無いかぎりこのままだろうね。

ClutterImage に画像セットはコレしか手段が無い、これは困った。
Gjs でいくなら ClutterCnavas で cairo という手しか無いっぽい。
でもそれなら GtkDrawinArea でいいじゃん、ということになり…

結論、Clutter で画像を使うのに Gjs は絶望的に向いていません。

G_PRIORITY_DEFAULT

連休なのに更新ができないままです。
しかたがない、Comipoli Gjs 版で行き詰まっているままですので。
何故こんなに遅いのかまだ解明できていない。

とりあえず他の部分だけでも仕上げておくか。
とやっていたらヒント?かもしれないことを発見。

g_idle_add でバックグラウンド動作ができない場合があった。

その部分だけ抜き出ししてサンプルコード。
PATH 定数は手持ちな巨大サイズの CBZ ファイルに書き換えてね。

const Lang = imports.lang;
const GLib = imports.gi.GLib;
const Gtk = imports.gi.Gtk;
const Gio = imports.gi.Gio;
const GdkPixbuf = imports.gi.GdkPixbuf;

const PATH = "/home/sasakima-nao/doc/comic/bigfile.cbz";
const SIZE = 120;

const NewArchive = new Lang.Class({
    Name: 'NewArchive',

    _init: function(path) {
        this.namelist = [];
        this.path = path;
        let l = path.split('.');
        let ext = l[l.length-1].toLowerCase();
        //
        if (ext == "cbz") {
            let sp = Gio.Subprocess.new(["unzip", "-Z", path], Gio.SubprocessFlags.STDOUT_PIPE);
            let istream = sp.get_stdout_pipe();
            let dstream = new Gio.DataInputStream({
                base_stream: istream
            });
            for (;;) {
                let [line, len] = dstream.read_line_utf8(null);
                if (line == null) break;
                if (line.startsWith('-')) {
                    let name = line.slice(53);
                    if (GLib.Regex.match_simple("\.(jpe?g|png|gif)$", name, GLib.RegexCompileFlags.CASELESS, 0)) {
                        this.namelist.push(name);
                    }
                }
            }
            this.namelist.sort();
        }
    },
    get_item: function(num) {
        let sp = Gio.Subprocess.new(["unzip", "-p", this.path, this.namelist[num]], Gio.SubprocessFlags.STDOUT_PIPE);
        let stream = sp.get_stdout_pipe();
        try {
            var pixbuf = GdkPixbuf.Pixbuf.new_from_stream(stream, null);
        }
        catch(e) {
            print(this.namelist[num]);
            print(e);
        }
        stream.close(null);
        return pixbuf;
    },
    get_length: function() {
        return this.namelist.length;
    }
});

const ThumbnailWindow = new Lang.Class({
    Name: 'ThumbnailDialog',
    Extends: Gtk.Window,

    _init: function() {
        this.parent();
        this.archive = new NewArchive(PATH);
        this.fbox = new Gtk.FlowBox({
            valign: Gtk.Align.START,
            max_children_per_line: 10,
            homogeneous: true,
            selection_mode: Gtk.SelectionMode.BROWSE
        });
        let scroll = new Gtk.ScrolledWindow();
        scroll.add(this.fbox);
        this.add(scroll);
        this.resize(1024, 480);
        this.show_all();
        // idle
        this.gen_func = this.read_zipfile();
        //
        // not G_PRIORITY_DEFAULT or G_PRIORITY_HIGH_IDLE
        //
        //GLib.idle_add(GLib.PRIORITY_DEFAULT, Lang.bind(this, function() {
        GLib.idle_add(GLib.PRIORITY_LOW, Lang.bind(this, function() {
            try {
                this.gen_func.next();
                return true;
            } catch(e) {
                return false;
            }
        }));
    },
    read_zipfile: function() {
        let l = this.archive.get_length();
        for (let i=0; i<l; i++) {
            let pixbuf = this.archive.get_item(i);
            this.append_data(pixbuf, i);
            yield 1;
        }
    },
    append_data: function(pixbuf, num) {
        if (pixbuf.get_width() > pixbuf.get_height()) {
            var pix_w = SIZE;
            var pix_h = Math.round(SIZE * pixbuf.get_height() / pixbuf.get_width());
        } else {
            var pix_w = Math.round(SIZE * pixbuf.get_width() / pixbuf.get_height());
            var pix_h = SIZE;
        }
        let minp = pixbuf.scale_simple(pix_w, pix_h, GdkPixbuf.InterpType.TILES);
        let image = new Gtk.Image({
            pixbuf: minp,
            visible: true
        });
        this.fbox.add(image);
    },
    vfunc_hide: function() {
        Gtk.main_quit();
    }
});

Gtk.init(null);
new ThumbnailWindow();
Gtk.main();

抜き出したのに長い…

最初は何も考えずに G_PRIORITY_DEFAULT 指定。
すると for 文で回した時のように全部展開されるまで表示されない。
これじゃ g_idle_add の意味が無い。

バックグラウンド動作させるための関数に何故こんな機能を付けたのだ?
てか、原因がコレだと解るのに一日使ったってばさ。
ということで、皆さん DEFAULT という定数名は疑ったほうがいいかも。

もしかして遅い原因もコレ関係かと探しているけど見当たらない。
this.read_zipfile = function(){}
みたく指定していたのを上記で書き換えたら少し早くなった、気がする…

明日から筆者は仕事だ!
そんなこんなで連休では完成できませんでした。

苗ちゃんフルマカロンまであと一つ、炭酸はギリギリ足りそうだ。
元浜公園に行ったらポケストップが無くなっていてしょんぼり。
いや、息抜きですよ。