IDEとか補完入力とかCopilotとか苦手だった私がChatGPTにすっかりハマってしまった件 〜 GStreamer-rsのSRTでも統計情報が見たい! 〜

# [前置き]実は私、IDEとか補完機能とかめちゃ苦手なんです😭
みなさん、こんにちは。すっかり寒くなりました。
そして世の中はChatGPTが流行ってますね。というわけで早速便乗してChatGPTを活用してみます。
と言っても、私は元々IDEはおろか補完機能やリファレンスジャンプの類が嫌いでCopilotも使っていませんでした。

AIが書くプログラミングなんてただの便利機能に過ぎないし、自分でソースコードを追っていかないとライブラリの構造も頭に入らないし。
必要なのはシンタックスハイライトとgrepとフォーマッタがあればそれで良いと思ってました。
そう。ChatGPTを使うまでは。
(いや、それでもnvimにRLSとか入れはしませんが)

## [やりたいこと]SRTがうまく送信できるのか確認したいんだけど、、、
(動画配信に明るくない方は、GStreamerというむつかしいライブラリをChatGPTに教えてもらう話だと思って読み飛ばしてください)
さて、きっかけとしてはGstreamer-rsでトランスコードするサーバーを書いているのですが、ある環境では遅延は1秒未満なのに、ある環境てば2秒遅延するということがありました。
GStreamerのパイプライン自体にはレイテンシーの概念などがあり、ツールを使ったりトレースログを出すことでそこに問題がないことはわかりました。

まあ、空海先生を筆頭に動画配信とは相性の悪いソフトはたくさんあるのでそのせいにしても良いのですが、ここでふと困ったことがありました。
一番最初の動画配信の送信箇所である `OBS Studio` から `GStreamer` へ配信するSRTの状況がよくわからないのです。

(参考 : PCからOBS Studioを使いSRTにて動画配信しています)

```plantuml
left to right direction

actor user
[ある環境 OBS Studio] as win_obs
[別の環境 OBS Studio] as mac_obs
[gstreamer-rs] as gst

user --> win_obs
user --> mac_obs
win_obs --> gst: SRTにて配信 遅い。。?
mac_obs --> gst: SRTにて配信 早い??
gst --> [配信]: ここは問題ない
gst --> gst: ここも問題ない
```

さて、`OBS Studio` でSRTの配信状況が確認できれば良いのですがそんな機能はないようです。(なんかツールはありましたが相手にいちいち入れてもらうのも大変)
というわけで、GStreamerで統計情報を見たいのです。

欲しいのはこれ。帯域とか転送速度とかRTT。
これをサーバー側でログに出したい。毎秒。

```plaintext
packets-received: 3791     // 受信パケット数
packets-received-lost: 0   // 喪失パケット数
bytes-received: 4806080     // 受信バイト数
bytes-received-lost: 0      // 喪失バイト数
receive-rate-mbps: 5.6      // 受信速度 Mbps
negotiated-latency-ms: 120  // SRTで設定したレイテンシー
bandwidth-mbps: 338.988     // 帯域?
rtt-ms: 12.148              // パケットの往復にかかる時間
```

# まずはどうしたら良いか、聞いてみる

とりあえず、やり方すらよく分からないので聞いてみます。
綺麗なスケルトンプログラムが出てきました。
<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/e3eee5c1-439f-67dd-0682-4128000b102f.png" />

が、回答はここで途切れてしまって、肝心の統計情報の取得については良くわかりませんでした。
(何度かやり直しても、TODO形式で表示されたり、存在しないメソッドを出力したり、違うエレメントの統計情報の取得を書き出したりなど)

何度かやり直しているとちゃんと教えてくれました。

<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/1714ca2f-5d50-56fe-e34d-a58d553d98c7.png" />

とりあえず、マニュアルを読みます。

#### マニュアルには詳しい情報が載ってない。。??

細かい統計情報くらい取れるよねー? え、ないの??

```
### SRTのプロパティ stats はあるが bytes-received-total しかないの??
“stats” GstStructure *

application/x-srt-statistics, bytes-received-total=(guint64)0;
```

https://gstreamer.freedesktop.org/documentation/srt/srtsrc.html?gi-language=c#srtsrc:stats

### ソースコードにあった

rttとか大体のやつはある。どうやら `stats` プロパティを見れば良いだけっぽい

https://gitlab.freedesktop.org/gstreamer/gstreamer/-/blob/main/subprojects/gst-plugins-bad/ext/srt/gstsrtobject.c#L1942-1985

# 具体的なコードをChatGPT聞いてみる

ということでやりたいことは、SRTエレメントの `stats` プロパティを毎秒読み取って表示するだけという何とも簡単な作業のようです。
基本のプログラムもあるし、30分で終わるよね。☺️

ChatGPTさんにお願いしてみます


> gstreamer-rs で srtserversrc の statsを1秒おきに表示するサンプルプログラムを書いてください

<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/84cd13c6-db64-a30f-0ba9-0ce69ddde022.png" />

うん。悪くない。
とりあえず、プロパティを取得する処理は入っているし、毎秒ループするようにもなっている。
ただし、ライブラリの情報が少し古いのと、こんなとこでループしたらメインの処理がブロックしてしまうので、通常のメッセージハンドリングもつけてもらうようにお願い。
(以下、基本的にはソースコードの要点のみ抜粋します)

> busからEOSやエラーの時の処理も追加してください。

チュートリアルなどでお馴染みのループ処理が書いてあるが、なんか違う。
ここは1秒と関係ない。

```rust
    let bus = pipeline.get_bus().unwrap();
    for msg in bus.timed_timed(Duration::from_secs(1)) {
        use gst::MessageView;
        match msg.view() {
            MessageView::Eos(..) => break,
            MessageView::Error(err) => {
                println!(
                    "Error from {:?}: {} ({:?})",
                    err.get_src().map(|s| s.get_path_string()),
                    err.get_error(),
                    err.get_debug()
                );
                break;
            }
            _ => (),
        }
    }
```

## 二つのイベントループ、もしくはマルチスレッドをうまく処理できるか

何が問題かというとイベント処理のためのループと毎秒処理を行うためのループが二つあるため、どちらか片方しかできない状態

```rust
loop { ... 毎秒、統計情報を取得して出力する処理 }

for msg in bus.iter_timed(...) {
  ... gstreamerのエラーハンドリングなどのイベントループ
}
```

ここまでなんだかんだで1時間
そしてガチャループの途中でマルチスレッドの正しいやり方を思い出す。

## GStreamerのイベントループを使ってくれるようにお願いする

> glib::MainContextを使ってイベントループを回すようにしてください

うん、いい感じ。 
型がちょっと違ったり、所有権の問題などがありますが細々修正してOK
コメントとかエラー時にスレッドを終了する処理とかはChatGPTさんが書いてくれました。
ここまで2時間くらい。かなり感心する。

```rust
fn main() -> Result<(), Error> {
    
... ハイプラインの初期化などは元からあるプログラム

    pipeline.set_state(gst::State::Playing)?;
    let stats = pipeline_cloned.by_name("srtserversrc").unwrap().property::<gst::Structure>("stats");

    // GStreamerのメインループを作成
    let main_context = glib::MainContext::default();
    let main_loop = glib::MainLoop::new(Some(&main_context), false);
    let main_loop_cloned = main_loop.clone();

    // srtserversrcのstatsを1秒ごとに表示する処理を設定
    let bus = pipeline.bus().unwrap();
    let timeout_id = glib::timeout_add_seconds(1, move || {
        let stats = property("stats").unwrap();
        println!("stats: {:?}", stats);
        glib::Continue(true)
    });

   bus.add_watch(move |_, msg| match msg.view() {
        gst::MessageView::Eos(..) => {
            // EOSの処理
            println!("EOS");
            main_loop_cloned.quit();
            gst::glib::Continue(false)
        }
        gst::MessageView::Error(err) => {
            // エラーの処理
            println!(
                "Error from {:?}: {} ({:?})",
                err.src().map(|s| s.path_string()),
                err.error(),
                err.debug()
            );
            main_loop_cloned.quit();
            gst::glib::Continue(false)
        }
        _ => gst::glib::Continue(true),
    });

    // GStreamerのメインループを実行
    main_loop.run();

    pipeline.set_state(gst::State::Null)?;

    Ok(())
}
```

これを動かすと1秒単位にSRTの統計情報が表示されます。
やったね!

```shell
Structure(application/x-srt-statistics { callers: (GValueArray) < application/x-srt-statistics, packets-received=(gint64)4296, packets-received-lost=(int)0, packet-ack-sent=(int)222, packet-nack-sent=(int)0, bytes-received=(guint64)5471448, bytes-received-lost=(guint64)0, receive-rate-mbps=(double)6.0450207238515707, negotiated-latency-ms=(int)120, bandwidth-mbps=(double)2352.9479999999999, rtt-ms=(double)13.420999999999999, caller-address=(GSocketAddress)NULL; >, bytes-received-total: (guint64) 5471448 })
bytes-received: 0, bytes-received-lost: 0, receive-rate-mbps: 0.0, negotiated-latency-ms: 0, bandwidth-mbps: 0.0, rtt-ms: 0
Structure(application/x-srt-statistics { callers: (GValueArray) < application/x-srt-statistics, packets-received=(gint64)5027, packets-received-lost=(int)0, packet-ack-sent=(int)273, packet-nack-sent=(int)0, bytes-received=(guint64)6412404, bytes-received-lost=(guint64)0, receive-rate-mbps=(double)6.2225093487627996, negotiated-latency-ms=(int)120, bandwidth-mbps=(double)1842.864, rtt-ms=(double)13.381, caller-address=(GSocketAddress)NULL; >, bytes-received-total: (guint64) 6412404 })
bytes-received: 0, bytes-received-lost: 0, receive-rate-mbps: 0.0, negotiated-latency-ms: 0, bandwidth-mbps: 0.0, rtt-ms: 0
Structure(application/x-srt-statistics { callers: (GValueArray) < application/x-srt-statistics, packets-received=(gint64)5697, packets-received-lost=(int)0, packet-ack-sent=(int)318, packet-nack-sent=(int)0, bytes-received=(guint64)7271528, bytes-received-lost=(guint64)0, receive-rate-mbps=(double)6.2950766309977331, negotiated-latency-ms=(int)120, bandwidth-mbps=(double)53.280000000000001, rtt-ms=(double)13.27, caller-address=(GSocketAddress)NULL; >, bytes-received-total: (guint64) 7271528 })
```

## ここからもうひと頑張り。必要な項目だけ表示したい!

とりあえずは出力できましたが不要な項目は省いて、必要な項目のみにしたいです。
ChatGPTさんにお願いしてみましょう。

> srtserversrc の stats プロパティで以下の項目だけ出力するよにしてください
> プログラムの該当箇所だけ出力してください
> 
> packets-received
> packets-received-lost
> bytes-received
> bytes-received-lost
> receive-rate-mbps
> negotiated-latency-ms
> bandwidth-mbps
> rtt-ms

お、いい感じ。
ちなみに、何がすごいって **stats.get::&lt;u64&gt;("bytes-received")** とか **stats.get::&lt;f64&gt;("receive-rate-mbps")** とかの
**型指定までちゃんとやってくれる** ってとこにびっくり。思わず変な笑いがでて、気付いたらスクショを撮ってしまったほど。
ここまで2時間半くらい。

(ちなみに運が良くないとこんなに綺麗に書いてくれない。でも「型指定して」とか「println()を一行にまとめて」とか言えばいい感じにやってくれる)

<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/ed1ff497-1e45-9f90-0dfd-fbbe8399aebd.png" />

## で、ここからChatGPTの本当の凄さを知る

多少手直しは必要だし、場合によっては明後日の方向に進んでいくこともあるものの、何度かやり直すか、コンパイルエラーをコピペするか、ドキュメントを読んで正しいメソッドを指定すればブロック単位ではしっかり動くようになる。

で、ここまできたものの期待どうりに動作しない。
出力が全て0になってしまう。(unwrap()じゃなくてデフォルトで0にして、とお願いして unwrap_or(0) をつけて実行している)

```shell
packets-received: 0, packets-received-lost: 0, bytes-received: 0, bytes-received-lost: 0, receive-rate-mbps: 0, negotiated-latency-ms: 0, bandwidth-mbps: 0, rtt-ms: 0
packets-received: 0, packets-received-lost: 0, bytes-received: 0, bytes-received-lost: 0, receive-rate-mbps: 0, negotiated-latency-ms: 0, bandwidth-mbps: 0, rtt-ms: 0
packets-received: 0, packets-received-lost: 0, bytes-received: 0, bytes-received-lost: 0, receive-rate-mbps: 0, negotiated-latency-ms: 0, bandwidth-mbps: 0, rtt-ms: 0
```

どうやら正しくフィールドを取得できていないみたい。
早速聞いてみる

<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/898cb2ec-ef9e-b914-780f-63b996743133.png" />

え、何でこんな質問の仕方でまともに返答が返ってくるんですかね。
思わずなるほどー。と思ってしまったけれど、残念、そこじゃなかった。

その後しばらく、ドキュメント読んだり、フィールドがあるかどうか確認する方法を聞いて、調べてみるとそんなフィールドはそもそも入っていないようだ。
公式ドキュメントの通り、 `bytes-received-total` しか `stats` には存在しない。
でも質問文に書いた通り、stats 自体を println すると色々なフィールドが入っている。なんで??

ん、待てよ。なんかデータが入れ子になってる??
データ構造どうなってるの? もう一度聞いてみよう

## ChatGPTの答えにただ感心するばかり

あ、うん。なるほど。 `callers` というフィールドの中に入ってたのね。
ってかなんでこんな聴き方で的確に答えてくれるの??

落ち着いてよく読めば良いだけの話であるのだけれど、疲れてる時にぐちゃっと表示されてるログなどは見落としが多くなりがち。人は見たいようにみる。
それをこれだけわかりやすく指摘してくれるのはホントすごい。
(ちなみにプログラムにはcallers入ってないやん)

<img width=640 src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/91710/6b934e02-9daf-4657-c2df-a9964ea6ba4b.png" />


で、`ValueArray` 型の取り扱いを調べたり聞いたりして結果出来上がり
無事に動いて、ここまで3時間か4時間くらい。

```rust
    // srtserversrcのstatsを1秒ごとに表示する処理を設定
    glib::source::timeout_add_seconds(
        1,
        move || {
            let stats = pipeline.by_name("srtserversrc").unwrap().property::<gst::Structure>("stats");

            if let Ok(callers) = stats.get::<gst::glib::ValueArray>("callers") {
                for value in callers.iter() {
                    if let Ok(caller) = value.get::<gst::Structure>() {
                        let packets_received = caller.get::<i64>("packets-received").unwrap_or(0);
                        let packets_received_lost = caller.get::<i32>("packets-received-lost").unwrap_or(0);
                        let bytes_received = caller.get::<u64>("bytes-received").unwrap_or(0);
                        let bytes_received_lost = caller.get::<u64>("bytes-received-lost").unwrap_or(0);
                        let receive_rate_mbps = caller.get::<f64>("receive-rate-mbps").unwrap_or(0.0);
                        let negotiated_latency_ms = caller.get::<i32>("negotiated-latency-ms").unwrap_or(0);
                        let bandwidth_mbps = caller.get::<f64>("bandwidth-mbps").unwrap_or(0.0);
                        let rtt_ms = caller.get::<f64>("rtt-ms").unwrap_or(0.0);
                        println!("packets-received: {}, packets-received-lost: {}, bytes-received: {}, bytes-received-lost: {}, receive-rate-mbps: {}, negotiated-latency-ms: {}, bandwidth-mbps: {}, rtt-ms: {}", packets_received, packets_received_lost, bytes_received, bytes_received_lost, receive_rate_mbps, negotiated_latency_ms, bandwidth_mbps, rtt_ms);
                    }
                }
            }

            glib::Continue(true)
        },
```

```shell
packets-received: 2688, packets-received-lost: 0, bytes-received: 3412596, bytes-received-lost: 0, receive-rate-mbps: 5.623549196019875, negotiated-latency-ms: 120, bandwidth-mbps: 2840.244, rtt-ms: 12.389
packets-received: 3071, packets-received-lost: 0, bytes-received: 3880648, bytes-received-lost: 0, receive-rate-mbps: 5.302910459832805, negotiated-latency-ms: 120, bandwidth-mbps: 1137.168, rtt-ms: 13.045
packets-received: 3791, packets-received-lost: 0, bytes-received: 4806080, bytes-received-lost: 0, receive-rate-mbps: 5.609430362258009, negotiated-latency-ms: 120, bandwidth-mbps: 338.988, rtt-ms: 12.148
```

# まとめ : ChatGPTは遠慮なく質問できるおっちょこちょいな先生

実は私、人に何かを聞くのも苦手なんです。
だって子供の頃に両親に色々聞きまくっていたら「人に質問する前に自分で調べなさい」と言われたので😢
そんなわけでソーシャルな空間とかどうも苦手なんですね。

というわけで、ChatGPTの一番良いところ、今までと違うところは **聞けば教えてくれる**　ところではないでしょうか。
マニュアルやドキュメントやソースコードを読むのは大事ですし、依然としてAIが要件通りのプログラムを書いてくれるわけではありません。
平気で間違った答えを返しますが、きちんと根拠が示されるのでそこを調べていくと問題が解決しやすいし理解も深まっていくと思います。

#### ChatGPTのすごいところ

- 根拠を説明してくれる
- 判断を下してくれる
    - 一昔前のチャットボットは「情報は表示するからあとは自分でよんで判断してね」というのがほとんどだった。
    - 例 : 「いつゴミを出せば良いか知りたい」-> bot「ゴミ出しの日は燃えるゴミはx曜日、萌えないごみはy曜日。自分の地域に合わせて出してね」
- 話の流れを覚えている
    - 「プログラムのみ出力してください」とか「xx行より後ろを表示してください」というとちゃんとやってくれる
    - コンパイルエラーを打ち込むだけでちゃんと理由とどう直せば良いかと、直したコードを教えてくれる

#### ChatGPTのおっちょこちょいなところ

- GStremaer のバージョン1.18以前の情報を返すことがある
- gstreamer-rs のバージョンを `0.20.0` だと言い出す (最新は0.19.3)
- ドキュメントのリンクが切れていたりする
- たまにドキュメントにない関数を返すことがある
- glib::* と gst::glib::* がごっちゃになることがある (自分で書いていてもよく間違う)
- イベントのループの中にイベントループを描くような、初心者がやりそうなコードを返してくることがある。
    - その場合はスレッドをリセットした方が良い
- 長い文章は途中で切れてしまうので、所有権チェック周りは自分で考えた方が早そう
- 二つお願いすると一つしか聞いてくれないことがある
- さながらコードレビューをしている気分

というわけで最後まで読んでいただきありがとうございました。

#### その後
続いてGKEについて、1nodeに2podずつ均等に割り当てる方法とか聞いてみましたが、レプリカセットを2にしろとしか教えてくれませんでした。
論理的に矛盾した回答が多かったので一緒に考えてくれるのは難しそうです。
知らないものと知ってるものの差も大きいのかな。