【Java】ffprobe -show_streamsの結果をJSONで出力し、Jacksonでオブジェクトにマップする

やることはタイトル通りですが、内容は主に`Jackson`の使い方の話です。

# ffprobeのオプション指定
`ffprobe`は以下のように動かします。

```bash
cat ${ファイル名} | ffprobe -hide_banner -v error -print_format json -show_streams  -i pipe:0
```

諸々指定していますが、重要なのは以下の2点です。

- `-print_format json` -> 処理結果をJSON形式で出力
- `-show_streams` -> 音、ビデオ、字幕等の各ストリーム情報を出力

`Java`からの呼び出し方は以下の記事を参照してください。

- [【Java】Processのstdinに入力する \- Qiita](https://qiita.com/wrongwrong/items/6c9dcc758c113f405518)

## 呼び出し結果
呼び出し結果は以下のとおりです（長いので折りたたみます）。

<details><summary>呼び出し結果</summary><div>

```json
{
    "streams": [
        {
            "index": 0,
            "codec_name": "h264",
            "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
            "profile": "Main",
            "codec_type": "video",
            "codec_time_base": "752/45075",
            "codec_tag_string": "avc1",
            "codec_tag": "0x31637661",
            "width": 1920,
            "height": 1080,
            "coded_width": 1920,
            "coded_height": 1088,
            "has_b_frames": 1,
            "sample_aspect_ratio": "1:1",
            "display_aspect_ratio": "16:9",
            "pix_fmt": "yuv420p",
            "level": 41,
            "color_range": "tv",
            "chroma_location": "left",
            "refs": 1,
            "is_avc": "true",
            "nal_length_size": "4",
            "r_frame_rate": "30000/1001",
            "avg_frame_rate": "45075/1504",
            "time_base": "1/30000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 601600,
            "duration": "20.053333",
            "bit_rate": "6653705",
            "bits_per_raw_sample": "8",
            "nb_frames": "601",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2014-03-30T07:09:03.000000Z",
                "language": "eng",
                "handler_name": "\u001fMainconcept Video Media Handler",
                "encoder": "AVC Coding"
            }
        },
        {
            "index": 1,
            "codec_name": "aac",
            "codec_long_name": "AAC (Advanced Audio Coding)",
            "profile": "LC",
            "codec_type": "audio",
            "codec_time_base": "1/48000",
            "codec_tag_string": "mp4a",
            "codec_tag": "0x6134706d",
            "sample_fmt": "fltp",
            "sample_rate": "48000",
            "channels": 2,
            "channel_layout": "stereo",
            "bits_per_sample": 0,
            "r_frame_rate": "0/0",
            "avg_frame_rate": "0/0",
            "time_base": "1/48000",
            "start_pts": 0,
            "start_time": "0.000000",
            "duration_ts": 962560,
            "duration": "20.053333",
            "bit_rate": "317375",
            "max_bit_rate": "317625",
            "nb_frames": "942",
            "disposition": {
                "default": 1,
                "dub": 0,
                "original": 0,
                "comment": 0,
                "lyrics": 0,
                "karaoke": 0,
                "forced": 0,
                "hearing_impaired": 0,
                "visual_impaired": 0,
                "clean_effects": 0,
                "attached_pic": 0,
                "timed_thumbnails": 0
            },
            "tags": {
                "creation_time": "2014-03-30T07:09:03.000000Z",
                "language": "eng",
                "handler_name": "#Mainconcept MP4 Sound Media Handler"
            }
        }
    ]
}
```
</div></details>

# Jacksonによるマップ
先程出したprobe結果をオブジェクトにマップしていきます。
出力はビデオ内の`stream`の配列とします。

## マップ先オブジェクト
今回は以下のオブジェクトにマップしていきます（`@Getter`と`@Setter`は`lombok`を想定して書いています）。
結果の基底は`stream`とした上で、`video`か`audio`か（あるいはそれ以外か）で型を分けて表現しています。

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "codec_type", defaultImpl = NoClass.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = FfprobeStream.VideoStream.class, name = "video"),
        @JsonSubTypes.Type(value = FfprobeStream.AudioStream.class, name = "audio")
})
@JsonIgnoreProperties(ignoreUnknown = true)
@Getter @Setter
abstract class FfprobeStream {
    private Double duration;
    private String codecName;

    @Getter @Setter
    static class VideoStream extends FfprobeStream {
        private Integer width;
        private Integer height;
    }

    @Getter @Setter
    static class AudioStream extends FfprobeStream {
        private String channelLayout;
    }
}
```

### 解説
以下のアノテーションで、`codec_type`の値によってマップ先の型を制御しています。
神オブジェクトを作ってそこにマップする手も無くはないですが、後が大変になりやすいので、マップ先の型の制御はやっておいた方がベターです。

```java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "codec_type", defaultImpl = NoClass.class)
@JsonSubTypes({
        @JsonSubTypes.Type(value = FfprobeStream.VideoStream.class, name = "video"),
        @JsonSubTypes.Type(value = FfprobeStream.AudioStream.class, name = "audio")
})
```

以下のアノテーションで、マップできないフィールドは無視しています。
今回は結果の内一部フィールドしか用意していないため、これを指定しなければマップできなかった場合にエラーになります。

```java
@JsonIgnoreProperties(ignoreUnknown = true)
```

## probe結果文字列からstreamの配列にマップする
以下のコードで、probe結果をビデオ内の`stream`の配列にマップできます。
詳細はコメントの通りです。

```java
static List<FfprobeStream> parseFfprobeResult(String ffprobeResult) throws IOException {
    ObjectMapper mapper = new ObjectMapper();
    // ffprobeが出力するのはPropertyNamingStrategy.SNAKE_CASEのJSONであるため、そのように指定する
    mapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);

    // JSON -> Javaオブジェクトの変換を実装
    Map<String, List<FfprobeStream>> map
            = mapper.readValue(ffprobeResult, new TypeReference<Map<String, List<FfprobeStream>>>() {});

    // パース結果はMapで出てくるため、パラメータ指定でGetして返す
    return map.getOrDefault("streams", Collections.emptyList());
}
```