platform_decoding: Add ffmpeg wrapper for MD5 comparison.

This binary wraps around ffmpeg shipping with images, outputting the
resulting hashes one per line. This allows us to reuse the same
platform decoding validation the V4L2 and VAAPI binaries use.

BUG=b:225233215
TEST=/usr/local/graphics/ffmpeg_md5sum
--video=/usr/local/videos/crowd_run_256X144_fr15_bd8_8buf_l1.ivf
--flags=-hwaccel --flags=vaapi

Change-Id: I0efb07984785a0cde36e636e54bf55bb9968acbd
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform/graphics/+/3586208
Commit-Queue: Jao-ke Chin-Lee <jchinlee@google.com>
Reviewed-by: Ilja Friedel <ihf@chromium.org>
Reviewed-by: Jao-ke Chin-Lee <jchinlee@google.com>
Reviewed-by: Miguel Casas-Sanchez <mcasas@chromium.org>
Tested-by: Jao-ke Chin-Lee <jchinlee@google.com>
diff --git a/src/platform_decoding/cmd/ffmpeg_md5sum/Makefile b/src/platform_decoding/cmd/ffmpeg_md5sum/Makefile
new file mode 100644
index 0000000..bf909b3
--- /dev/null
+++ b/src/platform_decoding/cmd/ffmpeg_md5sum/Makefile
@@ -0,0 +1,18 @@
+# Copyright 2022 The Chromium OS Authors. All rights reserved.
+# Use of this source code is governed by a BSD-style license that can be
+# found in the LICENSE file.
+
+GOCMD=go
+GOBUILD=$(GOCMD) build
+GOCLEAN=$(GOCMD) clean
+
+FFMPEG_MD5SUM_SRCS=main.go
+FFMPEG_MD5SUM_BIN=../../bin/ffmpeg_md5sum
+
+all: build
+build:
+	$(GOBUILD) -o $(FFMPEG_MD5SUM_BIN) $(FFMPEG_MD5SUM_SRCS)
+
+clean:
+	$(GOCLEAN)
+	rm -f -r $(BIN_DIR)
diff --git a/src/platform_decoding/cmd/ffmpeg_md5sum/README.md b/src/platform_decoding/cmd/ffmpeg_md5sum/README.md
new file mode 100644
index 0000000..3169faa
--- /dev/null
+++ b/src/platform_decoding/cmd/ffmpeg_md5sum/README.md
@@ -0,0 +1,34 @@
+# ffmpeg\_md5sum
+ffmpeg\_md5sum is a wrapper around ffmpeg that generates MD5 sums from decoding
+for each frame of the input video, and processes that (annotated) output to
+output one hash per line.
+
+# Args
+The wrapper accepts the following arguments:
+ * video: path to video to decode
+ * flags: additional flags to pass to ffmpeg. For flags whose specification
+  relies on space separation, order matters, and pass each token individually,
+  i.e.  pass `--flags -hwaccel --flags vaapi` to the wrapper in order to pass
+  `-hwaccel vaapi` to ffmpeg.
+
+# Details
+The arguments to ffmpeg that this wrapper pre-populates are
+`-hide_banner -loglevel verbose -vf format=pix_fmts=yuv420p -f framemd5 -`,
+which output MD5 sums to the stdout.
+
+# Build and deploy
+``` bash
+(inside) emerge-$BOARD graphics-utils-go
+(inside) cros deploy $DUT graphics-utils-go
+(dut) /usr/local/graphics/ffmpeg_md5sum --video=<video path> --flags=-hwaccel --flags=vaapi
+(inside) tast run $DUT video.PlatformDecoding.ffmpeg*
+```
+
+# Additional notes
+The wrapper is expected to output *only* MD5 sums, one per line, on success.
+Logs should be printed to stderr.
+
+The wrapper expects ffmpeg to output any number of header (hash-prefixed) lines,
+followed by space-separated lines, one per frame, in which the last token is the
+MD5 hash. Any modifications to the ffmpeg flags the wrapper passes should
+maintain this format.
diff --git a/src/platform_decoding/cmd/ffmpeg_md5sum/main.go b/src/platform_decoding/cmd/ffmpeg_md5sum/main.go
new file mode 100644
index 0000000..02bb16a
--- /dev/null
+++ b/src/platform_decoding/cmd/ffmpeg_md5sum/main.go
@@ -0,0 +1,113 @@
+// Copyright 2022 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ffmpeg_md5sum wraps ffmpeg to reoutput MD5 sums from decoding, one per line.
+
+package main
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"flag"
+	"fmt"
+	"os"
+	"os/exec"
+	"strings"
+)
+
+const ffmpegPath = "/usr/local/bin/ffmpeg"
+
+// multiFlags allow an alternative to passing space-separated flags, so that
+// --f v1 v2 can instead be passed as --f v1 --f v2.
+// The Golang flag package cannot parse space-sparated flags without enclosing
+// the values in quotation marks. However, the platform decoding tests nest
+// binary calls and flags, complicating quotations.
+// This alternative provides a way to unnest/unfold the flags.
+type multiFlags []string
+
+// String implements the flag interface.
+func (m *multiFlags) String() string {
+	return strings.Join(*m, " ")
+}
+
+// Set implements the flag interface.
+func (m *multiFlags) Set(value string) error {
+	*m = append(*m, value)
+	return nil
+}
+
+// parseHashes parses hashes from ffmpeg output.
+func parseHashes(stdout string) ([]string, error) {
+	lines := strings.Split(stdout, "\n")
+	hashes := make([]string, 0, len(lines))
+
+	for _, l := range lines {
+		l = strings.TrimSpace(l)
+		if len(l) == 0 || strings.HasPrefix(l, "#") {
+			continue
+		}
+
+		tok := strings.Split(l, ",")
+		hash := strings.TrimSpace(tok[len(tok)-1])
+		if len(hash) != 32 {
+			return nil, errors.New(fmt.Sprintf("expected MD5 sum, got %v", hash))
+		}
+		hashes = append(hashes, tok[len(tok)-1])
+	}
+	return hashes, nil
+}
+
+// exitWithError dumps all output to the appropriate streams and exits with errcode 1.
+func exitWithError(stdout, stderr string, err error) {
+	fmt.Fprintf(os.Stdout, stdout)
+	fmt.Fprintf(os.Stderr, stderr)
+	fmt.Fprintf(os.Stderr, err.Error())
+	os.Exit(1)
+}
+
+func main() {
+	flag.Usage = func() {
+		fmt.Fprintf(os.Stderr,
+			"ffmpeg_md5sum is a wrapper around ffmpeg that generates MD5 sums from "+
+				"decoding, and processes that (annotated) output to output instead one "+
+				"hash per line.\n\n"+
+				"Flags:\n"+
+				"\tvideo: Required. Path to video to decode.\n"+
+				"\tflags: Optional. Additional flags to ffmpeg. Pass space-separated\n"+
+				"\t       flags individually, i.e. `--flags -hwaccel --flags vaapi`\n"+
+				"\t       to pass `-hwaccel vaapi` to ffmpeg.\n")
+	}
+
+	var flags multiFlags
+	var video string
+	flag.Var(&flags, "flags", "additional flags to ffmpeg: for space-separated flags, pass each individually with --flags")
+	flag.StringVar(&video, "video", "", "path to video to decode")
+	flag.Parse()
+
+	args := append(flags, []string{
+		"-hide_banner",
+		"-loglevel", "verbose",
+		"-i", video,
+		"-vf", "format=pix_fmts=yuv420p",
+		"-f", "framemd5", "-",
+	}...)
+	ctx := context.Background()
+	cmd := exec.CommandContext(ctx, ffmpegPath, args...)
+
+	fmt.Fprintf(os.Stderr, "Running `%s %s`\n", ffmpegPath, strings.Join(args, " "))
+	var outbuf, errbuf bytes.Buffer
+	cmd.Stdout, cmd.Stderr = &outbuf, &errbuf
+	err := cmd.Run()
+	stdout, stderr := outbuf.String(), errbuf.String()
+	if err != nil {
+		exitWithError(stdout, stderr, err)
+	}
+
+	var hashes []string
+	if hashes, err = parseHashes(stdout); err != nil {
+		exitWithError(stdout, "", err)
+	}
+	fmt.Println(strings.Join(hashes, "\n"))
+}