Repository: codenoobforreal/clitools
Pull request #28: Dev
Author: login: codenoobforreal
Created: 2025-04-29T07:29:00Z | Merged: 2025-04-29T08:20:56Z



<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

- **New Features**
  - Added support for bit depth encoding and pixel format handling in video processing.
  - Introduced a utility to filter HEVC videos with the "hev1" tag.
  - Enhanced progress reporting with formatted output and ETA during encoding tasks.

- **Improvements**
  - Refined error handling with custom error classes for more precise feedback.
  - Improved user feedback with spinners and detailed error messages during encoding and QuickTime enabling tasks.
  - Updated command building for H.265 encoding to automatically set profiles and bit depths based on video metadata.
  - Enhanced test utilities for easier and more flexible test data creation.

- **Bug Fixes**
  - Fixed handling of empty or malformed ffprobe output and improved test coverage for edge cases.

- **Refactor**
  - Streamlined task execution and error reporting throughout the CLI and encoding workflows.
  - Modularized processing logic for better maintainability.

- **Chores**
  - Removed an obsolete GitHub Actions workflow for syncing repository metadata.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->


Diff:
diff --git a/.changeset/cool-eels-fry.md b/.changeset/cool-eels-fry.md
new file mode 100644
index 0000000..2af933f
--- /dev/null
+++ b/.changeset/cool-eels-fry.md
@@ -0,0 +1,5 @@
+---
+"@codenoobforreal/clitools": minor
+---
+
+support bit depth encoding
diff --git a/.github/workflows/sync-metadata.yml b/.github/workflows/sync-metadata.yml
deleted file mode 100644
index b4048eb..0000000
--- a/.github/workflows/sync-metadata.yml
+++ /dev/null
@@ -1,45 +0,0 @@
-name: Sync Repo Metadata
-
-on:
-  push:
-    branches: [master]
-    paths:
-      - "package.json"
-
-concurrency: ${{ github.workflow }}-${{ github.ref }}
-
-jobs:
-  update-metadata:
-    runs-on: ubuntu-latest
-    permissions:
-      contents: write
-    steps:
-      - name: Checkout code
-        uses: actions/checkout@v4
-
-      - name: Extract package.json info
-        id: package-info
-        run: |
-          echo "description=$(jq -r '.description' package.json)" >> $GITHUB_OUTPUT
-          echo "keywords=$(jq -c '.keywords' package.json)" >> $GITHUB_OUTPUT
-
-      - name: Update Repository Description
-        uses: actions/github-script@v7
-        with:
-          script: |
-            github.rest.repos.update({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              description: '${{ steps.package-info.outputs.description }}'
-            })
-
-      - name: Update Repository Topics
-        uses: actions/github-script@v7
-        with:
-          script: |
-            const keywords = JSON.parse('${{ steps.package-info.outputs.keywords }}');
-            github.rest.repos.replaceAllTopics({
-              owner: context.repo.owner,
-              repo: context.repo.repo,
-              names: keywords.filter(word => word.length <= 35).slice(0, 20)
-            })
diff --git a/fixtures/ffprobe output.txt b/fixtures/ffprobe output.txt
index b36fb03..6edf433 100644
--- a/fixtures/ffprobe output.txt	
+++ b/fixtures/ffprobe output.txt	
@@ -22,7 +22,9 @@ film_grain=0
 has_b_frames=2
 sample_aspect_ratio=1:1
 display_aspect_ratio=1280:681
+
 pix_fmt=yuv420p
+
 level=180
 color_range=tv
 color_space=bt709
@@ -43,7 +45,9 @@ duration_ts=75264
 duration=5.880000
 bit_rate=19354759
 max_bit_rate=N/A
+
 bits_per_raw_sample=N/A
+
 nb_frames=147
 nb_read_frames=N/A
 nb_read_packets=N/A
diff --git a/src/core/ffmpeg/checker.ts b/src/core/ffmpeg/checker.ts
index 420769e..da3197f 100644
--- a/src/core/ffmpeg/checker.ts
+++ b/src/core/ffmpeg/checker.ts
@@ -1,5 +1,6 @@
 import { platform } from "node:process";
 import which from "which";
+import { MissingBinariesError } from "../../error.js";
 
 export function checkFFmpegInstallation(): void {
   const missingTools: string[] = [];
@@ -41,6 +42,6 @@ export function checkFFmpegInstallation(): void {
       `Installation recommendation:\n${installationCommand}\n` +
       "Note: ffprobe is typically included in ffmpeg packages. Ensure your package manager installs both tools.";
 
-    throw new Error(errorMessage);
+    throw new MissingBinariesError(errorMessage);
   }
 }
diff --git a/src/core/ffmpeg/command-builder.ts b/src/core/ffmpeg/command-builder.ts
index b6fb92c..b2455a8 100644
--- a/src/core/ffmpeg/command-builder.ts
+++ b/src/core/ffmpeg/command-builder.ts
@@ -1,11 +1,8 @@
 export class FFmpegCommandBuilder {
-  private globalOptions: string[] = ["-hide_banner", "-loglevel", "error"];
-  private inputs: string[] = [];
-  private outputOptions: string[] = [];
-  private outputFile: string = "";
-  private encoderToEncodeParamsKeyMap = {
-    libx265: "-x265-params" as const,
-  };
+  protected globalOptions: string[] = ["-hide_banner", "-loglevel", "error"];
+  protected inputs: string[] = [];
+  protected outputOptions: string[] = [];
+  protected outputFile: string = "";
 
   addProgressReporting(progressOutput: string = "pipe:2"): this {
     this.globalOptions.push("-progress", progressOutput);
@@ -17,14 +14,8 @@ export class FFmpegCommandBuilder {
     return this;
   }
 
-  setVideoEncoder(encoder: string, encoderParams?: string[]): this {
+  setVideoEncoder(encoder: string): this {
     this.outputOptions.push("-c:v", encoder);
-    if (encoderParams) {
-      this.outputOptions.push(
-        this.encoderToEncodeParamsKeyMap["libx265"],
-        ...encoderParams,
-      );
-    }
     return this;
   }
 
@@ -43,11 +34,16 @@ export class FFmpegCommandBuilder {
     return this;
   }
 
-  setPreset(preset: string = "veryfast"): this {
+  setPreset(preset: string = "medium"): this {
     this.outputOptions.push("-preset", preset);
     return this;
   }
 
+  setPixFmt(fmt: string = "yuv420p"): this {
+    this.outputOptions.push("-pix_fmt", fmt);
+    return this;
+  }
+
   setOutputFormat(format: string): this {
     this.outputOptions.push("-f", format);
     return this;
@@ -73,6 +69,69 @@ export class FFmpegCommandBuilder {
   }
 }
 
+export class FFmpegH265CommandBuilder extends FFmpegCommandBuilder {
+  private x265Params: string[] = [];
+  /** TODO：yuv422p and yuv444p mapping might be wrong */
+  protected pixFmtToProfileMap: Record<string, string> = {
+    yuv420p: "main",
+    yuv420p10le: "main10",
+    yuv420p12le: "main12",
+    yuv422p: "main",
+    yuv422p10le: "main422-10",
+    yuv422p12le: "main422-12",
+    yuv444p: "main",
+    yuv444p10le: "main444-10",
+    yuv444p12le: "main444-12",
+  };
+
+  constructor() {
+    super();
+    super.setVideoEncoder("libx265");
+  }
+
+  // eslint-disable-next-line @typescript-eslint/no-unused-vars
+  override setVideoEncoder(_encode: string): this {
+    return this;
+  }
+
+  setLogLevel(level: string = "error"): this {
+    this.x265Params.push(`log-level=${level}`);
+    return this;
+  }
+
+  setProfileByPixFmt(pix_fmt: string = "yuv420p"): this {
+    const profile = this.pixFmtToProfileMap[pix_fmt];
+
+    if (profile) {
+      this.x265Params.push(`profile=${profile}`);
+      return this;
+    }
+
+    // let encode decided
+    return this;
+  }
+
+  setInputDepth(depth: number = 8): this {
+    this.x265Params.push(`input-depth=${depth}`);
+    return this;
+  }
+
+  /**
+   * will derive the output bit depth from the profile name if --output-depth is not specified.
+   */
+  setOutputDepth(depth: number): this {
+    this.x265Params.push(`output-depth=${depth}`);
+    return this;
+  }
+
+  applyX265Params(): this {
+    if (this.x265Params.length > 0) {
+      this.outputOptions.push("-x265-params", this.x265Params.join(":"));
+    }
+    return this;
+  }
+}
+
 export class FFprobeCommandBuilder {
   private globalOptions: string[] = [];
   private outputFormat: string[] = [];
diff --git a/src/core/image/encode.ts b/src/core/image/encode.ts
index 05fdcef..030cfeb 100644
--- a/src/core/image/encode.ts
+++ b/src/core/image/encode.ts
@@ -2,6 +2,7 @@ import fs from "node:fs";
 import { pipeline } from "node:stream";
 import util from "node:util";
 import sharp from "sharp";
+import { EncodeImageError, NotSupportedError } from "../../error.js";
 import { generateOutputPath } from "../../utils/output-generator.js";
 import { getFileExt } from "../../utils/path.js";
 
@@ -9,56 +10,53 @@ const promisifyPipeline = util.promisify(pipeline);
 
 export async function encodeImage(input: string) {
   try {
-    return await new Promise<string>((resolve, reject) => {
-      const ext = getFileExt(input).toLowerCase();
-      const outputPath = generateOutputPath(input, ext);
-
-      const readStream = fs.createReadStream(input);
-      const writeStream = fs.createWriteStream(outputPath);
-
-      let processor: sharp.Sharp;
-
-      switch (ext) {
-        case "jpg":
-        case "jpeg":
-          processor = sharp().jpeg({
-            quality: 100,
-            optimiseScans: true,
-            mozjpeg: true,
-          });
-          break;
-        case "png":
-          processor = sharp().png({
-            compressionLevel: 9,
-            adaptiveFiltering: true,
-            effort: 10,
-          });
-          break;
-        case "webp":
-          processor = sharp().webp({
-            lossless: true,
-            quality: 100,
-            effort: 6,
-          });
-          break;
-        case "gif":
-          processor = sharp().gif({
-            interPaletteMaxError: 0,
-            dither: 0,
-            effort: 10,
-            reuse: true,
-            progressive: false,
-          });
-          break;
-        default:
-          throw new Error(`Not supported extension: ${ext}`);
-      }
-
-      promisifyPipeline(readStream, processor, writeStream)
-        .then(() => resolve(outputPath))
-        .catch(reject);
-    });
+    const ext = getFileExt(input).toLowerCase();
+    const outputPath = generateOutputPath(input, ext);
+    const readStream = fs.createReadStream(input);
+    const writeStream = fs.createWriteStream(outputPath);
+    let processor: sharp.Sharp;
+    switch (ext) {
+      case "jpg":
+      case "jpeg":
+        processor = sharp().jpeg({
+          quality: 100,
+          optimiseScans: true,
+          mozjpeg: true,
+        });
+        break;
+      case "png":
+        processor = sharp().png({
+          compressionLevel: 9,
+          adaptiveFiltering: true,
+          effort: 10,
+        });
+        break;
+      case "webp":
+        processor = sharp().webp({
+          lossless: true,
+          quality: 100,
+          effort: 6,
+        });
+        break;
+      case "gif":
+        processor = sharp().gif({
+          interPaletteMaxError: 0,
+          dither: 0,
+          effort: 10,
+          reuse: true,
+          progressive: false,
+        });
+        break;
+      default:
+        throw new NotSupportedError(`Not supported extension: ${ext}`);
+    }
+    await promisifyPipeline(readStream, processor, writeStream);
+    return outputPath;
   } catch (error) {
-    console.error(error);
+    if (error instanceof NotSupportedError || error instanceof Error) {
+      throw new EncodeImageError(error.message, error);
+    } else {
+      throw error;
+    }
   }
 }
diff --git a/src/core/image/pipeline.ts b/src/core/image/pipeline.ts
index 0ed3e8d..2cd118c 100644
--- a/src/core/image/pipeline.ts
+++ b/src/core/image/pipeline.ts
@@ -1,3 +1,4 @@
+import { NothingToProcessError } from "../../error.js";
 import { resolveAndNormalizePath } from "../../utils/path.js";
 import { sanitizePathLikeInput } from "../../utils/sanitize.js";
 import { getImagePathsFromPath } from "./collector.js";
@@ -11,7 +12,7 @@ export async function getImageListFromUserInput(
     await getImagePathsFromPath(normalizedPath);
   // TODO: scan error class
   if (collectedImagePaths.length === 0) {
-    throw new Error("no image to process");
+    throw new NothingToProcessError("no image to process");
   }
   return collectedImagePaths;
 }
diff --git a/src/core/video/metadata.test.ts b/src/core/video/metadata.test.ts
index 8cde6c8..b763408 100644
--- a/src/core/video/metadata.test.ts
+++ b/src/core/video/metadata.test.ts
@@ -1,5 +1,7 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
+import { FFprobeProcessError } from "../../error";
 import { runFFprobeCommand } from "../../libs/ffmpeg-executor";
+import { createFFprobeResultConvertdResult } from "../../utils/test-utils";
 import {
   buildFFprobeMetadataArgs,
   calcFFprobeFps,
@@ -21,54 +23,57 @@ beforeEach(() => {
 
 describe("convertFFprobeResult", () => {
   const validInput = `
-    codec_name=h264
-    codec_tag_string=avc1
+    codec_name=hevc
+    codec_tag_string=hev1
     width=1920
     height=1080
-    duration=60.5
-    bit_rate=5000000
-    avg_frame_rate=30/1
+    pix_fmt=yuv420p
+    duration=100
+    bits_per_raw_sample=8
   `;
 
   it("Verifies all fields are parsed correctly", () => {
     const result = convertFFprobeResult(validInput);
-
-    expect(result).toEqual({
-      codec_name: "h264",
-      codec_tag_string: "avc1",
-      width: 1920,
-      height: 1080,
-      duration: 60.5,
-      bit_rate: 5000000,
-      avg_frame_rate: 30,
-    });
+    expect(result).toEqual(createFFprobeResultConvertdResult());
   });
 
   it("Handles inputs containing empty lines", () => {
     const input = `
       codec_name=hevc
-
       codec_tag_string=hvc1
       width=3840
       height=2160
+      pix_fmt=yuv420p
       duration=120
-      bit_rate=20000000
-      avg_frame_rate=60000/1001
-    `;
 
+      bits_per_raw_sample=8
+
+    `;
     const result = convertFFprobeResult(input);
     expect(result.height).toBe(2160);
   });
 
+  it("Handles key= line without throw error", () => {
+    const input = `
+      codec_name=hevc
+      codec_tag_string=hvc1
+      width=3840
+      height=2160
+      pix_fmt=yuv420p
+      duration=120
+      bits_per_raw_sample=8
+      empty=
+    `;
+    expect(() => convertFFprobeResult(input)).not.toThrowError();
+  });
+
   it("Missing required fields validation", () => {
     const input = `
       codec_name=aac
       width=1280
       height=720
       duration=30
-      bit_rate=256000
     `;
-
     expect(() => convertFFprobeResult(input)).toThrow(
       /Missing required fields/,
     );
@@ -76,10 +81,7 @@ describe("convertFFprobeResult", () => {
 
   it("Invalid numeric format handling", () => {
     const input = validInput.replace("width=1920", "width=abc");
-
-    expect(() => convertFFprobeResult(input)).toThrow(
-      "Invalid width value: abc",
-    );
+    expect(() => convertFFprobeResult(input)).toThrow(/Invalid width value/);
   });
 
   it("Malformed key-value pair detection", () => {
@@ -87,7 +89,6 @@ describe("convertFFprobeResult", () => {
       { input: "invalidLine", error: "Invalid key-value format" },
       { input: "=value", error: "Invalid key-value format" },
     ];
-
     testCases.forEach(({ input, error }) => {
       expect(() => convertFFprobeResult(input)).toThrow(error);
     });
@@ -99,39 +100,6 @@ describe("convertFFprobeResult", () => {
   });
 });
 
-// describe("convertFFprobeResult", () => {
-//   it("should correctly parse all expected fields", () => {
-//     const input = `codec_name=hevc\ncodec_tag_string=hev1\nwidth=720\nheight=480\nduration=100.5\nnb_frames=2400\navg_frame_rate=24/1\nbit_rate=1`;
-//     const expected: FFprobeResultConvertdResult = {
-//       width: 720,
-//       height: 480,
-//       duration: 100.5,
-//       avg_frame_rate: 24,
-//       bit_rate: 1,
-//       codec_name: "hevc",
-//       codec_tag_string: "hev1",
-//     };
-//     const result = convertFFprobeResult(input);
-//     expect(result).toEqual(expected);
-//   });
-//   it("should throw error for Empty key", () => {
-//     const input = "=210";
-//     expect(() =>
-//       convertFFprobeResult(input),
-//     ).toThrowErrorMatchingInlineSnapshot(
-//       `[Error: Empty key in key-value pair: =210]`,
-//     );
-//   });
-//   it("should throw error for invalid lines", () => {
-//     const input = "invalid_line";
-//     expect(() =>
-//       convertFFprobeResult(input),
-//     ).toThrowErrorMatchingInlineSnapshot(
-//       `[Error: Missing equals sign in key-value pair: invalid_line]`,
-//     );
-//   });
-// });
-
 describe("getVideoMetadata", () => {
   it("should return converted result on valid output", async () => {
     const mockOutput = [
@@ -139,41 +107,40 @@ describe("getVideoMetadata", () => {
       "codec_tag_string=hev1",
       "width=1920",
       "height=1080",
-      "avg_frame_rate=24/1",
-      "duration=10.0",
-      "bit_rate=5",
+      "pix_fmt=yuv420p",
+      "duration=100",
+      "bits_per_raw_sample=8",
     ].join("\n");
     vi.mocked(runFFprobeCommand).mockResolvedValue({
       out: mockOutput,
       err: "",
     });
     const result = await getVideoMetadata("valid.mp4");
-    expect(result).toEqual({
-      codec_name: "hevc",
-      codec_tag_string: "hev1",
-      width: 1920,
-      height: 1080,
-      avg_frame_rate: 24,
-      duration: 10,
-      bit_rate: 5,
-    });
+    expect(result).toEqual(createFFprobeResultConvertdResult());
   });
 
-  it("should return null when runFFprobeCommand return undefined", async () => {
+  it("should return null when runFFprobeCommand throw error", async () => {
     const videoPath = "./nonexistent.mp4";
-    vi.mocked(runFFprobeCommand).mockResolvedValue(undefined);
-    await expect(getVideoMetadata(videoPath)).resolves.toBeNull();
+    vi.mocked(runFFprobeCommand).mockRejectedValue(
+      new FFprobeProcessError(
+        "ffprobe process error",
+        new Error("internal error"),
+      ),
+    );
+    await expect(getVideoMetadata(videoPath)).rejects.toThrow(
+      FFprobeProcessError,
+    );
   });
 
-  it("should throw error when ffprobe output is empty", async () => {
+  it("should return null when ffprobe output is empty", async () => {
     vi.mocked(runFFprobeCommand).mockImplementationOnce(async () => ({
       out: "",
       err: "ffprobe error",
     }));
     const videoPath = "./test.mp4";
-    await expect(
-      getVideoMetadata(videoPath),
-    ).rejects.toThrowErrorMatchingInlineSnapshot(`[Error: ffprobe error]`);
+    await expect(getVideoMetadata(videoPath)).rejects.toThrow(
+      FFprobeProcessError,
+    );
   });
 });
 
diff --git a/src/core/video/metadata.ts b/src/core/video/metadata.ts
index ec0ecc2..29903a0 100644
--- a/src/core/video/metadata.ts
+++ b/src/core/video/metadata.ts
@@ -1,20 +1,23 @@
+import {
+  FFprobeProcessError,
+  KVError,
+  RequiredFieldError,
+  StringToNumberConvertError,
+} from "../../error.js";
 import { runFFprobeCommand } from "../../libs/ffmpeg-executor.js";
 import type { FFprobeResultConvertdResult } from "../../types.js";
+import { tryConvertStringToNumber } from "../../utils/basic-convert.js";
 import { FFprobeCommandBuilder } from "../ffmpeg/command-builder.js";
 
 export async function getVideoMetadata(
   videoPath: string,
 ): Promise<FFprobeResultConvertdResult | null> {
   const getVideoMetaDataArgs = buildFFprobeMetadataArgs(videoPath);
-  const res = await runFFprobeCommand(getVideoMetaDataArgs);
-  if (res === undefined) {
-    return null;
-  }
-  const { out, err } = res;
+
+  const { out, err } = await runFFprobeCommand(getVideoMetaDataArgs);
   // out will be empty string and err will be error message when ffprobe got error
   if (out === "") {
-    // TODO: custom ffprobe error
-    throw new Error(err);
+    throw new FFprobeProcessError("FFprobe handle error", new Error(err));
   }
   return convertFFprobeResult(out);
 }
@@ -30,23 +33,25 @@ export function convertFFprobeResult(
     "codec_tag_string",
     "width",
     "height",
+    "pix_fmt",
+    // "avg_frame_rate",
     "duration",
-    "bit_rate",
-    "avg_frame_rate",
+    // "bit_rate",
+    "bits_per_raw_sample",
   ]);
 
   const parseKeyValue = (line: string): [string, string] => {
     const eqIndex = line.indexOf("=");
     if (eqIndex === -1 || eqIndex === 0) {
-      throw new Error(`Invalid key-value format: ${line}`);
+      throw new KVError("Invalid key-value format", line);
     }
     return [line.slice(0, eqIndex), line.slice(eqIndex + 1)];
   };
 
   const validateNumber = (value: string, key: string): number => {
-    const num = Number(value);
-    if (!value.trim() || Number.isNaN(num)) {
-      throw new Error(`Invalid ${key} value: ${value}`);
+    const num = tryConvertStringToNumber(value);
+    if (!num) {
+      throw new StringToNumberConvertError(`Invalid ${key} value`, value);
     }
     return num;
   };
@@ -60,6 +65,7 @@ export function convertFFprobeResult(
     switch (key) {
       case "codec_name":
       case "codec_tag_string":
+      case "pix_fmt":
         resultObject[key] = value;
         requiredFields.delete(key);
         break;
@@ -67,22 +73,27 @@ export function convertFFprobeResult(
       case "width":
       case "height":
       case "duration":
-      case "bit_rate":
+        // case "bit_rate":
         resultObject[key] = validateNumber(value, key);
         requiredFields.delete(key);
         break;
-
-      case "avg_frame_rate":
-        resultObject.avg_frame_rate = calcFFprobeFps(value);
-        requiredFields.delete("avg_frame_rate");
+      case "bits_per_raw_sample": {
+        resultObject[key] = tryConvertStringToNumber(value) ?? 8;
+        requiredFields.delete(key);
         break;
+
+        // case "avg_frame_rate":
+        //   resultObject.avg_frame_rate = calcFFprobeFps(value);
+        //   requiredFields.delete("avg_frame_rate");
+        //   break;
+      }
     }
   }
 
   if (requiredFields.size > 0) {
-    throw new Error(
-      `Missing required fields: ${[...requiredFields].join(", ")}`,
-    );
+    throw new RequiredFieldError("Missing required fields", [
+      ...requiredFields,
+    ]);
   }
 
   return resultObject as FFprobeResultConvertdResult;
diff --git a/src/core/video/pipeline.test.ts b/src/core/video/pipeline.test.ts
index 5a14aa7..3dc9b90 100644
--- a/src/core/video/pipeline.test.ts
+++ b/src/core/video/pipeline.test.ts
@@ -1,12 +1,16 @@
 import os from "node:os";
 import process from "node:process";
 import { beforeEach, describe, expect, it, vi } from "vitest";
-import { FFprobeResultConvertdResult } from "../../types";
+import { NothingToProcessError } from "../../error";
 import { resolveAndNormalizePath } from "../../utils/path";
 import { sanitizePathLikeInput } from "../../utils/sanitize";
+import {
+  createFFprobeResultConvertdResult,
+  createVideoInfo,
+} from "../../utils/test-utils";
 import { getVideoPathsFromPath } from "./collector";
 import { getVideoMetadata } from "./metadata";
-import { getVideoInfoListFromUserInput } from "./pipeline";
+import { filterHev1Video, getVideoInfoListFromUserInput } from "./pipeline";
 
 vi.mock(import("./collector"), async (importOriginal) => {
   const actual = await importOriginal();
@@ -46,15 +50,7 @@ describe("getVideoInfoListFromUserInput", () => {
     vi.mocked(getVideoMetadata).mockResolvedValue(mockMetadata);
   });
 
-  const mockMetadata: FFprobeResultConvertdResult = {
-    width: 720,
-    height: 480,
-    duration: 100.5,
-    avg_frame_rate: 24,
-    bit_rate: 1,
-    codec_name: "hevc",
-    codec_tag_string: "hev1",
-  };
+  const mockMetadata = createFFprobeResultConvertdResult();
 
   it("should process valid directory input and return video infos", async () => {
     const results = await getVideoInfoListFromUserInput("/valid/directory");
@@ -84,19 +80,15 @@ describe("getVideoInfoListFromUserInput", () => {
     vi.mocked(getVideoPathsFromPath).mockResolvedValue([]);
     await expect(
       getVideoInfoListFromUserInput("/empty/directory"),
-    ).rejects.toThrow("no video to process");
+    ).rejects.toThrow(NothingToProcessError);
   });
 
   it("should return successful results with error logging", async () => {
     vi.mocked(getVideoMetadata)
       .mockResolvedValueOnce(mockMetadata)
       .mockRejectedValueOnce(new Error("Corrupted file"));
-    const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
     const results = await getVideoInfoListFromUserInput("/valid/directory");
     expect(results).toHaveLength(1);
-    expect(consoleSpy).toHaveBeenCalledWith(
-      expect.stringContaining("Corrupted file"),
-    );
   });
 
   it("should enhance error messages with video path", async () => {
@@ -105,12 +97,86 @@ describe("getVideoInfoListFromUserInput", () => {
     vi.mocked(getVideoPathsFromPath).mockResolvedValue([
       "/normalized/path/error.mp4",
     ]);
-    const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
     await expect(
       getVideoInfoListFromUserInput("/valid/directory"),
-    ).resolves.toEqual([]);
-    expect(consoleSpy).toHaveBeenCalledWith(
-      "Processing failed: [/normalized/path/error.mp4] Test error",
-    );
+    ).rejects.toThrow(NothingToProcessError);
+  });
+});
+
+describe("filterHev1Video", () => {
+  it("should return videos with hevc codec and hev1 tag", () => {
+    const videos = [
+      createVideoInfo({
+        metadata: {
+          codec_name: "hevc",
+          codec_tag_string: "hev1",
+        },
+      }),
+      createVideoInfo({
+        metadata: {
+          codec_name: "hevc",
+          codec_tag_string: "hvc1",
+        },
+      }),
+      createVideoInfo({
+        metadata: {
+          codec_name: "avc1",
+          codec_tag_string: "hev1",
+        },
+      }),
+      createVideoInfo({
+        metadata: {
+          codec_name: "avc1",
+          codec_tag_string: "avc1",
+        },
+      }),
+      // createVideoInfo("hevc", "hev1"),
+      // createVideoInfo("hevc", "hvc1"),
+      // createVideoInfo("avc1", "hev1"),
+      // createVideoInfo("avc1", "avc1"),
+    ];
+    const result = filterHev1Video(videos);
+    expect(result).toMatchObject([videos[0]]);
+  });
+
+  it("should return empty array when no videos match criteria", () => {
+    const videos = [
+      createVideoInfo({
+        metadata: {
+          codec_name: "avc1",
+          codec_tag_string: "hvc1",
+        },
+      }),
+      createVideoInfo({
+        metadata: {
+          codec_name: "vp9",
+          codec_tag_string: "hev1",
+        },
+      }),
+      // createVideoInfo("avc1", "hvc1"),
+      // createVideoInfo("vp9", "hev1"),
+    ];
+    expect(filterHev1Video(videos)).toEqual([]);
+  });
+
+  it("should handle empty input array", () => {
+    expect(filterHev1Video([])).toEqual([]);
+  });
+
+  it("should strictly check both codec and tag", () => {
+    const partialMatch1 = createVideoInfo({
+      metadata: {
+        codec_name: "hevc",
+        codec_tag_string: "hvc1",
+      },
+    });
+    const partialMatch2 = createVideoInfo({
+      metadata: {
+        codec_name: "avc1",
+        codec_tag_string: "hev1",
+      },
+    });
+    const result = filterHev1Video([partialMatch1, partialMatch2]);
+    expect(result).toEqual([]);
   });
 });
diff --git a/src/core/video/pipeline.ts b/src/core/video/pipeline.ts
index ff875d7..7bacad9 100644
--- a/src/core/video/pipeline.ts
+++ b/src/core/video/pipeline.ts
@@ -1,5 +1,9 @@
 import os from "node:os";
 import pLimit from "p-limit";
+import {
+  ExtractVideoMetadataError,
+  NothingToProcessError,
+} from "../../error.js";
 import type { VideoInfo } from "../../types.js";
 import { resolveAndNormalizePath } from "../../utils/path.js";
 import { sanitizePathLikeInput } from "../../utils/sanitize.js";
@@ -13,20 +17,18 @@ export async function getMetadataToVideoList(videoPaths: string[]) {
   const processSingleVideo = async (videoPath: string) => {
     try {
       const metadata = await getVideoMetadata(videoPath);
-
       if (!metadata) {
-        throw new Error(`Metadata extraction failed for ${videoPath}`);
+        throw new ExtractVideoMetadataError("Metadata extraction failed");
       }
-
       return {
         input: videoPath,
         metadata,
       };
     } catch (error) {
       const normalizedError =
-        error instanceof Error
+        error instanceof ExtractVideoMetadataError
           ? error
-          : new Error(`Unknown error: ${String(error)}`);
+          : new Error(`Unknown error`, { cause: error });
       normalizedError.message = `[${videoPath}] ${normalizedError.message}`;
       throw normalizedError;
     }
@@ -36,24 +38,20 @@ export async function getMetadataToVideoList(videoPaths: string[]) {
     const tasks = videoPaths.map((path) =>
       limit(() => processSingleVideo(path)),
     );
-
     const results = await Promise.allSettled(tasks);
-
     const successfulResults: VideoInfo[] = [];
-    const errorLogger = console.error.bind(console);
-
+    // const errorLogger = console.error.bind(console);
     results.forEach((result) => {
       if (result.status === "fulfilled") {
         successfulResults.push(result.value);
       } else {
-        errorLogger(`Processing failed: ${result.reason.message}`);
         // TODO: report error
+        // errorLogger(`Processing failed: ${result.reason.message}`);
       }
     });
-
     return successfulResults;
-  } catch (error) {
-    console.error("Unexpected pipeline error:", error);
+  } catch {
+    // console.error("Unexpected pipeline error:", error);
     return [];
   }
 }
@@ -65,9 +63,22 @@ export async function getVideoInfoListFromUserInput(
   const normalizedPath = resolveAndNormalizePath(sanitizedPath, process.cwd());
   const collectedVideoPaths: string[] =
     await getVideoPathsFromPath(normalizedPath);
-  // TODO: scan error class
   if (collectedVideoPaths.length === 0) {
-    throw new Error("no video to process");
+    throw new NothingToProcessError("No supported video files found");
+  }
+  const videoInfoList = await getMetadataToVideoList(collectedVideoPaths);
+  if (videoInfoList.length === 0) {
+    throw new NothingToProcessError(
+      "No processable videos with valid metadata available",
+    );
   }
-  return await getMetadataToVideoList(collectedVideoPaths);
+  return videoInfoList;
+}
+
+export function filterHev1Video(videoInfoList: VideoInfo[]) {
+  return videoInfoList.filter(
+    (v) =>
+      v.metadata.codec_name === "hevc" &&
+      v.metadata.codec_tag_string === "hev1",
+  );
 }
diff --git a/src/error.ts b/src/error.ts
new file mode 100644
index 0000000..ad574da
--- /dev/null
+++ b/src/error.ts
@@ -0,0 +1,98 @@
+class MyError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = this.constructor.name;
+  }
+}
+
+export class SpawnProcessError extends MyError {
+  public override cause: Error;
+  public stderr: string;
+  constructor(message: string, stderr: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+    this.stderr = stderr;
+  }
+}
+
+export class FFmpegProcessError extends MyError {
+  public override cause: Error;
+  constructor(message: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+  }
+}
+
+export class FFprobeProcessError extends FFmpegProcessError {}
+
+export class KVError extends MyError {
+  public pair: string;
+  constructor(message: string, pair: string) {
+    super(message);
+    this.pair = pair;
+  }
+}
+
+export class StringToNumberConvertError extends MyError {
+  public value: string;
+  constructor(message: string, value: string) {
+    super(message);
+    this.value = value;
+  }
+}
+
+export class FFprobeResultConvertError extends MyError {
+  public override cause: Error;
+  constructor(message: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+  }
+}
+
+export class EncodeImageError extends MyError {
+  public override cause: Error;
+  constructor(message: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+  }
+}
+
+export class ExtractVideoMetadataError extends MyError {
+  constructor(message: string) {
+    super(message);
+  }
+}
+
+export class RequiredFieldError extends MyError {
+  public fields: string[];
+  constructor(message: string, fields: string[]) {
+    super(message);
+    this.fields = fields;
+  }
+}
+
+export class MissingBinariesError extends MyError {
+  constructor(message: string) {
+    super(message);
+  }
+}
+
+export class NotSupportedError extends MyError {
+  constructor(message: string) {
+    super(message);
+  }
+}
+
+export class NothingToProcessError extends MyError {
+  constructor(message: string) {
+    super(message);
+  }
+}
+
+export class UnknownTaskError extends MyError {
+  public task: string;
+  constructor(message: string, task: string) {
+    super(message);
+    this.task = task;
+  }
+}
diff --git a/src/features/cli/index.test.ts b/src/features/cli/index.test.ts
index 5ed3e33..28f7429 100644
--- a/src/features/cli/index.test.ts
+++ b/src/features/cli/index.test.ts
@@ -1,21 +1,26 @@
 import { beforeEach, describe, expect, it, vi } from "vitest";
+import { checkFFmpegInstallation } from "../../core/ffmpeg/checker";
 import { askForContinue } from "../../libs/prompt";
 import { enableHEVCQuickTimeTask } from "../enable-quickTime";
-import { imageEncodeTask } from "../encode-image";
-import { videoEncodeTask } from "../encode-video";
+import { encodeImageTask } from "../encode-image";
+import { encodeVideoTask } from "../encode-video";
 import { runCli } from "./index";
 import { getTaskDetail } from "./task-detail";
 
+vi.mock("../../core/ffmpeg/checker", () => ({
+  checkFFmpegInstallation: vi.fn(),
+}));
+
 vi.mock("../../libs/prompt", () => ({
   askForContinue: vi.fn(),
 }));
 
 vi.mock("../encode-video", () => ({
-  videoEncodeTask: vi.fn(),
+  encodeVideoTask: vi.fn(),
 }));
 
 vi.mock("../encode-image", () => ({
-  imageEncodeTask: vi.fn(),
+  encodeImageTask: vi.fn(),
 }));
 
 vi.mock("../enable-quickTime", () => ({
@@ -40,9 +45,9 @@ describe("runCli", () => {
     vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
     vi.mocked(askForContinue).mockResolvedValue(false);
     await runCli();
-    expect(videoEncodeTask).not.toHaveBeenCalled();
+    expect(encodeVideoTask).not.toHaveBeenCalled();
     expect(enableHEVCQuickTimeTask).not.toHaveBeenCalled();
-    expect(imageEncodeTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).not.toHaveBeenCalled();
   });
 
   it("should execute video encode task with parameters", async () => {
@@ -51,12 +56,32 @@ describe("runCli", () => {
       task: "video-encode" as const,
       answer: mockAnswer,
     };
+    vi.mocked(checkFFmpegInstallation).mockReturnValue();
     vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
     vi.mocked(askForContinue).mockResolvedValue(true);
     await runCli();
-    expect(videoEncodeTask).toHaveBeenCalledWith(mockAnswer);
+    expect(checkFFmpegInstallation).toHaveBeenCalled();
+    expect(encodeVideoTask).toHaveBeenCalledWith(mockAnswer);
+    expect(enableHEVCQuickTimeTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).not.toHaveBeenCalled();
+  });
+
+  it("should throw error when executing video encode task with ffmpeg check failed", async () => {
+    const mockAnswer = { input: "test.mp4" };
+    const mockTaskDetail = {
+      task: "video-encode" as const,
+      answer: mockAnswer,
+    };
+    vi.mocked(checkFFmpegInstallation).mockImplementation(() => {
+      throw new Error("ffmpeg not installed");
+    });
+    vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
+    vi.mocked(askForContinue).mockResolvedValue(true);
+    await expect(runCli()).rejects.toThrow(Error);
+    expect(checkFFmpegInstallation).toHaveBeenCalled();
+    expect(encodeVideoTask).not.toHaveBeenCalledWith(mockAnswer);
     expect(enableHEVCQuickTimeTask).not.toHaveBeenCalled();
-    expect(imageEncodeTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).not.toHaveBeenCalled();
   });
 
   it("should execute HEVC QuickTime enable task", async () => {
@@ -65,12 +90,32 @@ describe("runCli", () => {
       task: "hevc-enable-QuickTime" as const,
       answer: mockAnswer,
     };
+    vi.mocked(checkFFmpegInstallation).mockReturnValue();
     vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
     vi.mocked(askForContinue).mockResolvedValue(true);
     await runCli();
+    expect(checkFFmpegInstallation).toHaveBeenCalled();
     expect(enableHEVCQuickTimeTask).toHaveBeenCalled();
-    expect(videoEncodeTask).not.toHaveBeenCalled();
-    expect(imageEncodeTask).not.toHaveBeenCalled();
+    expect(encodeVideoTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).not.toHaveBeenCalled();
+  });
+
+  it("should throw error when executing HEVC QuickTime enable task with ffmpeg check failed", async () => {
+    const mockAnswer = { input: "test.mp4" };
+    const mockTaskDetail = {
+      task: "hevc-enable-QuickTime" as const,
+      answer: mockAnswer,
+    };
+    vi.mocked(checkFFmpegInstallation).mockImplementation(() => {
+      throw new Error("ffmpeg not installed");
+    });
+    vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
+    vi.mocked(askForContinue).mockResolvedValue(true);
+    await expect(runCli()).rejects.toThrow(Error);
+    expect(checkFFmpegInstallation).toHaveBeenCalled();
+    expect(encodeVideoTask).not.toHaveBeenCalledWith(mockAnswer);
+    expect(enableHEVCQuickTimeTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).not.toHaveBeenCalled();
   });
 
   it("should execute image encode task with parameters", async () => {
@@ -82,8 +127,8 @@ describe("runCli", () => {
     vi.mocked(getTaskDetail).mockResolvedValue(mockTaskDetail);
     vi.mocked(askForContinue).mockResolvedValue(true);
     await runCli();
-    expect(imageEncodeTask).toHaveBeenCalledWith(mockAnswer);
-    expect(videoEncodeTask).not.toHaveBeenCalled();
+    expect(encodeImageTask).toHaveBeenCalledWith(mockAnswer);
+    expect(encodeVideoTask).not.toHaveBeenCalled();
     expect(enableHEVCQuickTimeTask).not.toHaveBeenCalled();
   });
 });
diff --git a/src/features/cli/index.ts b/src/features/cli/index.ts
index ebf6da2..20f67ef 100644
--- a/src/features/cli/index.ts
+++ b/src/features/cli/index.ts
@@ -1,24 +1,26 @@
+import { checkFFmpegInstallation } from "../../core/ffmpeg/checker.js";
 import { askForContinue } from "../../libs/prompt.js";
 import { enableHEVCQuickTimeTask } from "../enable-quickTime/index.js";
-import { imageEncodeTask } from "../encode-image/index.js";
-import { videoEncodeTask } from "../encode-video/index.js";
+import { encodeImageTask } from "../encode-image/index.js";
+import { encodeVideoTask } from "../encode-video/index.js";
 import { getTaskDetail } from "./task-detail.js";
 
+const taskHandlers = {
+  "video-encode": { checkFFmpeg: true, handler: encodeVideoTask },
+  "hevc-enable-QuickTime": {
+    checkFFmpeg: true,
+    handler: enableHEVCQuickTimeTask,
+  },
+  "image-encode": { checkFFmpeg: false, handler: encodeImageTask },
+};
+
 export async function runCli() {
   const { task, answer } = await getTaskDetail();
   const shouldContinue = await askForContinue();
   if (!shouldContinue) {
     return;
   }
-  switch (task) {
-    case "video-encode":
-      await videoEncodeTask(answer);
-      break;
-    case "hevc-enable-QuickTime":
-      await enableHEVCQuickTimeTask(answer);
-      break;
-    case "image-encode":
-      await imageEncodeTask(answer);
-      break;
-  }
+  const config = taskHandlers[task];
+  if (config.checkFFmpeg) checkFFmpegInstallation();
+  await config.handler(answer);
 }
diff --git a/src/features/cli/progress-handler.test.ts b/src/features/cli/progress-handler.test.ts
new file mode 100644
index 0000000..932b578
--- /dev/null
+++ b/src/features/cli/progress-handler.test.ts
@@ -0,0 +1,80 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { ProgressInfo } from "../../types";
+import { createProgressHandler } from "./progress-handler";
+
+describe("createProgressHandler", () => {
+  beforeEach(() => {
+    vi.clearAllMocks();
+    vi.useRealTimers();
+  });
+
+  const mockMessageHandler = {
+    message: vi.fn(),
+  };
+
+  it('should handle "end" event by sending 100% completion', () => {
+    const handler = createProgressHandler(100, mockMessageHandler);
+    handler({ progress: "end", out_time_ms: 1_000_000, speed: 1 });
+    expect(mockMessageHandler.message).toBeCalledWith("[100%] eta: 00:00:00");
+  });
+
+  it("should skip processing when missing critical fields", () => {
+    const handler = createProgressHandler(100, mockMessageHandler);
+    handler({ speed: 1 } as ProgressInfo);
+    handler({ out_time_ms: 1_000_000 } as ProgressInfo);
+    handler({});
+    expect(mockMessageHandler.message).not.toBeCalled();
+  });
+
+  it("should throttle updates to once per second", () => {
+    vi.useFakeTimers();
+    const handler = createProgressHandler(100, mockMessageHandler);
+    const baseTime = Date.now();
+
+    vi.setSystemTime(baseTime);
+    handler({ out_time_ms: 0, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toHaveBeenCalledTimes(1);
+
+    vi.advanceTimersByTime(500);
+    handler({ out_time_ms: 50_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toHaveBeenCalledTimes(1);
+
+    vi.advanceTimersByTime(1100);
+    handler({ out_time_ms: 100_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toHaveBeenCalledTimes(2);
+  });
+
+  it("should calculate percentage correctly with upper limit", () => {
+    const handler = createProgressHandler(100, mockMessageHandler);
+    handler({ out_time_ms: 150_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toBeCalledWith(
+      expect.stringContaining("99.99%"),
+    );
+  });
+
+  it("should calculate ETA correctly and prevent negative values", () => {
+    const handler = createProgressHandler(100, mockMessageHandler);
+    handler({ out_time_ms: 150_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toBeCalledWith(
+      expect.stringContaining("00:00:00"),
+    );
+  });
+
+  it("should format progress message with correct padding", () => {
+    vi.useFakeTimers();
+    const handler = createProgressHandler(100, mockMessageHandler);
+    const baseTime = Date.now();
+
+    vi.setSystemTime(baseTime);
+    handler({ out_time_ms: 5_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toBeCalledWith(
+      "[  5.00%] eta: 00:01:35",
+    );
+
+    vi.advanceTimersByTime(2000);
+    handler({ out_time_ms: 75_000_000, speed: 1 } as ProgressInfo);
+    expect(mockMessageHandler.message).toBeCalledWith(
+      "[ 75.00%] eta: 00:00:25",
+    );
+  });
+});
diff --git a/src/features/cli/progress-handler.ts b/src/features/cli/progress-handler.ts
index 99ec9dc..56cb3ef 100644
--- a/src/features/cli/progress-handler.ts
+++ b/src/features/cli/progress-handler.ts
@@ -1,15 +1,21 @@
 import type { ProgressInfo } from "../../types.js";
 import { formatSeconds } from "../../utils/date.js";
 
-export function createProgressHandler(totalDuration: number) {
+export function createProgressHandler(
+  totalDuration: number,
+  messageHandler: {
+    message: (msg?: string) => void;
+  },
+) {
   let lastUpdate = 0;
   let remaining = 0;
-  // const p = progress({ max: 100 });
+
   return (progress: ProgressInfo) => {
-    if (!progress.out_time_ms || progress.speed === undefined) return;
+    if (progress.out_time_ms === undefined || progress.speed === undefined)
+      return;
 
     if (progress.progress === "end") {
-      // log.message(`100%`);
+      messageHandler.message(`[100%] eta: 00:00:00`);
       return;
     }
 
@@ -29,8 +35,9 @@ export function createProgressHandler(totalDuration: number) {
       );
     }
 
-    // eslint-disable-next-line @typescript-eslint/no-unused-vars
     const eta = formatSeconds(remaining);
-    // log.message(`[${percentage.toFixed(1).padStart(5)}%] eta: ${eta}`);
+    messageHandler.message(
+      `[${percentage.toFixed(2).padStart(6)}%] eta: ${eta}`,
+    );
   };
 }
diff --git a/src/features/enable-quickTime/args.test.ts b/src/features/enable-quickTime/args.test.ts
index 4901ec7..80f5135 100644
--- a/src/features/enable-quickTime/args.test.ts
+++ b/src/features/enable-quickTime/args.test.ts
@@ -1,5 +1,5 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { VideoInfo } from "../../types";
+import { createVideoInfo } from "../../utils/test-utils";
 import { buildHEVCEnableQuickTimeArgs } from "./args";
 
 describe("buildHEVCEnableQuickTimeArgs", () => {
@@ -11,21 +11,15 @@ describe("buildHEVCEnableQuickTimeArgs", () => {
   });
   it("should build command with basic configuration", () => {
     vi.setSystemTime(new Date("2000-01-01T00:00:00"));
-    const config: VideoInfo = {
-      input: "input.mp4",
-      metadata: {
-        width: 1920,
-        height: 1080,
-        avg_frame_rate: 25,
-        duration: 100,
-        bit_rate: 5,
-        codec_name: "hevc",
-        codec_tag_string: "hev1",
-      },
-    };
-    const result = buildHEVCEnableQuickTimeArgs(config);
-    expect(result.join(" ")).toMatchInlineSnapshot(
-      `"-hide_banner -loglevel error -i input.mp4 -c:v copy -f mp4 -tag:v hvc1 -c:a copy input-20000101000000.mp4"`,
-    );
+    const config = createVideoInfo();
+    const resultString = buildHEVCEnableQuickTimeArgs(config).join(" ");
+
+    expect(resultString).toMatch(/-hide_banner -loglevel error/);
+    expect(resultString).toMatch(/-i input.mp4/);
+    expect(resultString).toMatch(/-c:v copy/);
+    expect(resultString).toMatch(/-f mp4/);
+    expect(resultString).toMatch(/-tag:v hvc1/);
+    expect(resultString).toMatch(/-c:a copy/);
+    expect(resultString).toMatch(/input-20000101000000.mp4/);
   });
 });
diff --git a/src/features/enable-quickTime/index.ts b/src/features/enable-quickTime/index.ts
index f544044..bb11b5e 100644
--- a/src/features/enable-quickTime/index.ts
+++ b/src/features/enable-quickTime/index.ts
@@ -1,35 +1,48 @@
-import { log } from "@clack/prompts";
-import { checkFFmpegInstallation } from "../../core/ffmpeg/checker.js";
-import { getVideoInfoListFromUserInput } from "../../core/video/pipeline.js";
+import { spinner } from "@clack/prompts";
+import {
+  filterHev1Video,
+  getVideoInfoListFromUserInput,
+} from "../../core/video/pipeline.js";
+import { FFmpegProcessError, NothingToProcessError } from "../../error.js";
 import { runFFmpegCommand } from "../../libs/ffmpeg-executor.js";
 import type { EnableHEVCQuickTimeTaskProps, VideoInfo } from "../../types.js";
+import { getFileNameFromPath } from "../../utils/path.js";
 import { buildHEVCEnableQuickTimeArgs } from "./args.js";
 
 export async function enableHEVCQuickTimeTask({
   input,
 }: EnableHEVCQuickTimeTaskProps) {
-  checkFFmpegInstallation();
-
   const videoInfoList: VideoInfo[] = await getVideoInfoListFromUserInput(input);
-
-  const filterHev1Videos = videoInfoList.filter(
-    (v) =>
-      v.metadata.codec_name === "hevc" &&
-      v.metadata.codec_tag_string === "hev1",
-  );
-
-  for (let index = 0; index < filterHev1Videos.length; index++) {
-    const videoInfo = filterHev1Videos[index];
+  const filteredVideos = filterHev1Video(videoInfoList);
+  if (filteredVideos.length === 0) {
+    throw new NothingToProcessError("No hev1 videos found");
+  }
+  for (let index = 0; index < filteredVideos.length; index++) {
+    const videoInfo = filteredVideos[index];
     if (videoInfo !== undefined) {
-      log.message(
-        `processing ${index + 1}/${filterHev1Videos.length}:\n${videoInfo.input}`,
-      );
-      const result = await runFFmpegCommand(
-        buildHEVCEnableQuickTimeArgs(videoInfo),
-      );
-      if (result) {
-        log.success("complete!");
-      }
+      await enableHEVCQuickTime(videoInfo, index, filteredVideos.length);
+    }
+  }
+}
+
+async function enableHEVCQuickTime(
+  videoInfo: VideoInfo,
+  index: number,
+  length: number,
+) {
+  const s = spinner();
+  const filename = getFileNameFromPath(videoInfo.input);
+  s.start(`Remuxing: ${filename}`);
+  try {
+    await runFFmpegCommand(buildHEVCEnableQuickTimeArgs(videoInfo));
+    s.stop(`Finish remuxing ${index + 1}/${length}:\n${videoInfo.input}`);
+  } catch (error) {
+    let errMessage: string = `Failed to remux: ${videoInfo.input}\nerror: `;
+    if (error instanceof FFmpegProcessError) {
+      errMessage += error.cause.message;
+    } else {
+      errMessage += "unknown error";
     }
+    s.stop(errMessage);
   }
 }
diff --git a/src/features/encode-image/index.ts b/src/features/encode-image/index.ts
index f6dfed4..4147deb 100644
--- a/src/features/encode-image/index.ts
+++ b/src/features/encode-image/index.ts
@@ -1,34 +1,27 @@
 import { spinner } from "@clack/prompts";
 import { encodeImage } from "../../core/image/encode.js";
 import { getImageListFromUserInput } from "../../core/image/pipeline.js";
+import { EncodeImageError } from "../../error.js";
 import { createConcurrencyLimit } from "../../libs/concurrency.js";
 import type { EncodeImageTaskProps } from "../../types.js";
 
-export async function imageEncodeTask({ input }: EncodeImageTaskProps) {
+export async function encodeImageTask({ input }: EncodeImageTaskProps) {
   const s = spinner();
   s.start(`Encoding images in: ${input}`);
   const imageList: string[] = await getImageListFromUserInput(input);
   const limit = createConcurrencyLimit();
   const tasks = imageList.map((imagePath) =>
-    limit(() =>
-      encodeImage(imagePath).catch((error) => {
-        throw new Error(`Failed to process: ${imagePath}`, {
-          cause: error,
-        });
-      }),
-    ),
+    limit(() => encodeImage(imagePath)),
   );
   const results = await Promise.allSettled(tasks);
   const successList: string[] = [];
   const failureList: Array<Error> = [];
   results.forEach((result) => {
     if (result.status === "fulfilled") {
-      if (result.value) {
-        successList.push(result.value);
-      }
+      successList.push(result.value);
     } else {
       const error =
-        result.reason instanceof Error
+        result.reason instanceof EncodeImageError
           ? result.reason
           : new Error(`Unknown error: ${JSON.stringify(result.reason)}`);
       failureList.push(error);
diff --git a/src/features/encode-video/args.test.ts b/src/features/encode-video/args.test.ts
index 7ff1d8a..b6210fe 100644
--- a/src/features/encode-video/args.test.ts
+++ b/src/features/encode-video/args.test.ts
@@ -1,5 +1,5 @@
 import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { VideoInfo } from "../../types";
+import { createVideoInfo } from "../../utils/test-utils";
 import { buildFFmpegEncodeVideoArgs } from "./args";
 
 describe("buildFFmpegEncodeVideoArgs", () => {
@@ -11,21 +11,52 @@ describe("buildFFmpegEncodeVideoArgs", () => {
   });
   it("should build command with basic configuration", () => {
     vi.setSystemTime(new Date("2000-01-01T00:00:00"));
-    const config: VideoInfo = {
-      input: "input.mp4",
+    const config = createVideoInfo();
+    const resultString = buildFFmpegEncodeVideoArgs(config).join(" ");
+
+    expect(resultString).toMatch(
+      /-hide_banner -loglevel error -progress pipe:2/,
+    );
+    expect(resultString).toMatch(/-i input.mp4/);
+    expect(resultString).toMatch(/-c:v libx265/);
+    expect(resultString).toMatch(/-pix_fmt yuv420p/);
+    expect(resultString).toMatch(
+      /-x265-params log-level=error:profile=main:input-depth=8/,
+    );
+    expect(resultString).toMatch(/-crf 20/);
+    expect(resultString).toMatch(/-f mp4/);
+    expect(resultString).toMatch(/-c:a copy/);
+    expect(resultString).toMatch(/input-20000101000000.mp4/);
+  });
+
+  it("should respect pix_fmt", () => {
+    vi.setSystemTime(new Date("2000-01-01T00:00:00"));
+    let config = createVideoInfo({
       metadata: {
-        width: 1920,
-        height: 1080,
-        avg_frame_rate: 25,
-        duration: 100,
-        bit_rate: 5,
-        codec_name: "hevc",
-        codec_tag_string: "hev1",
+        pix_fmt: "yuv420p10le",
       },
-    };
-    const result = buildFFmpegEncodeVideoArgs(config);
-    expect(result.join(" ")).toMatchInlineSnapshot(
-      `"-hide_banner -loglevel error -progress pipe:2 -i input.mp4 -c:v libx265 -x265-params log-level=error -crf 20 -preset veryfast -f mp4 -c:a copy input-20000101000000.mp4"`,
-    );
+    });
+    let resultString = buildFFmpegEncodeVideoArgs(config).join(" ");
+    expect(resultString).toMatch(/profile=main10/);
+    expect(resultString).toMatch(/-pix_fmt yuv420p10le/);
+
+    config = createVideoInfo({
+      metadata: {
+        pix_fmt: "yuv422p10le",
+      },
+    });
+    resultString = buildFFmpegEncodeVideoArgs(config).join(" ");
+    expect(resultString).toMatch(/profile=main422-10/);
+    expect(resultString).toMatch(/-pix_fmt yuv422p10le/);
+
+    config = createVideoInfo({
+      metadata: {
+        pix_fmt: "yuv444p10le",
+      },
+    });
+
+    resultString = buildFFmpegEncodeVideoArgs(config).join(" ");
+    expect(resultString).toMatch(/profile=main444-10/);
+    expect(resultString).toMatch(/-pix_fmt yuv444p10le/);
   });
 });
diff --git a/src/features/encode-video/args.ts b/src/features/encode-video/args.ts
index 4e990f9..19ede2a 100644
--- a/src/features/encode-video/args.ts
+++ b/src/features/encode-video/args.ts
@@ -1,4 +1,4 @@
-import { FFmpegCommandBuilder } from "../../core/ffmpeg/command-builder.js";
+import { FFmpegH265CommandBuilder } from "../../core/ffmpeg/command-builder.js";
 import { calculateCrfByPixelCount } from "../../core/video/crf-calculation.js";
 import type { VideoInfo } from "../../types.js";
 import { generateOutputPath } from "../../utils/output-generator.js";
@@ -7,18 +7,21 @@ export function buildFFmpegEncodeVideoArgs({
   metadata,
   input,
 }: VideoInfo): string[] {
-  const { width, height } = metadata;
+  const { width, height, pix_fmt, bits_per_raw_sample } = metadata;
   const crf = calculateCrfByPixelCount(width * height);
   // TODO: format depends on encode requirement
   const format = "mp4";
   const output = generateOutputPath(input, format);
 
-  return new FFmpegCommandBuilder()
+  return new FFmpegH265CommandBuilder()
     .addProgressReporting()
     .addInput(input)
-    .setVideoEncoder("libx265", ["log-level=error"])
+    .setPixFmt(pix_fmt)
+    .setLogLevel()
+    .setProfileByPixFmt(pix_fmt)
+    .setInputDepth(bits_per_raw_sample)
+    .applyX265Params()
     .setCrf(crf)
-    .setPreset()
     .setOutputFormat(format)
     .copyAudio()
     .setOutput(output)
diff --git a/src/features/encode-video/index.ts b/src/features/encode-video/index.ts
index fd6763c..7e9730e 100644
--- a/src/features/encode-video/index.ts
+++ b/src/features/encode-video/index.ts
@@ -1,29 +1,47 @@
-import { log } from "@clack/prompts";
-import { checkFFmpegInstallation } from "../../core/ffmpeg/checker.js";
+import { spinner } from "@clack/prompts";
 import { getVideoInfoListFromUserInput } from "../../core/video/pipeline.js";
+import { FFmpegProcessError } from "../../error.js";
 import { runFFmpegCommand } from "../../libs/ffmpeg-executor.js";
 import type { EncodeVideoTaskProps, VideoInfo } from "../../types.js";
+import { getFileNameFromPath } from "../../utils/path.js";
 import { createProgressHandler } from "../cli/progress-handler.js";
 import { buildFFmpegEncodeVideoArgs } from "./args.js";
 
-export async function videoEncodeTask({ input }: EncodeVideoTaskProps) {
-  checkFFmpegInstallation();
-
+export async function encodeVideoTask({ input }: EncodeVideoTaskProps) {
   const videoInfoList: VideoInfo[] = await getVideoInfoListFromUserInput(input);
-
   for (let index = 0; index < videoInfoList.length; index++) {
     const videoInfo = videoInfoList[index];
     if (videoInfo !== undefined) {
-      log.message(
-        `processing ${index + 1}/${videoInfoList.length}:\n${videoInfo.input}`,
-      );
-      const result = await runFFmpegCommand(
-        buildFFmpegEncodeVideoArgs(videoInfo),
-        createProgressHandler(videoInfo.metadata.duration),
-      );
-      if (result) {
-        log.success("complete!");
-      }
+      await encodeVideo(videoInfo, index, videoInfoList.length);
+    }
+  }
+}
+
+async function encodeVideo(
+  videoInfo: VideoInfo,
+  index: number,
+  length: number,
+) {
+  const s = spinner();
+  const filename = getFileNameFromPath(videoInfo.input);
+  s.start(`Preparing FFmpeg staff`);
+  try {
+    await runFFmpegCommand(
+      buildFFmpegEncodeVideoArgs(videoInfo),
+      createProgressHandler(videoInfo.metadata.duration, {
+        message: (msg?: string) => {
+          s.message(`Encoding ${filename}: ${msg}`);
+        },
+      }),
+    );
+    s.stop(`Finish encoding ${index + 1}/${length}:\n${videoInfo.input}`);
+  } catch (error) {
+    let errMessage: string = `Failed to encode: ${videoInfo.input}\nerror: `;
+    if (error instanceof FFmpegProcessError) {
+      errMessage += error.cause.message;
+    } else {
+      errMessage += "unknown error";
     }
+    s.stop(errMessage);
   }
 }
diff --git a/src/index.ts b/src/index.ts
index 9a1457b..563f2c6 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,5 +1,7 @@
 #!/usr/bin/env node
 
+import { log } from "@clack/prompts";
+import { NothingToProcessError } from "./error.js";
 import { runCli } from "./features/cli/index.js";
 import { isErrnoException } from "./types.js";
 
@@ -15,10 +17,10 @@ async function main() {
     // ELOOP	Symbolic link loop
     if (isErrnoException(error)) {
       if (error.code === "ENOENT") {
-        console.log(`no such path:\n${error.path}`);
+        log.error(`no such path:\n${error.path}`);
       }
-    } else {
-      console.log(error);
+    } else if (error instanceof NothingToProcessError) {
+      log.error(error.message);
     }
   }
 }
diff --git a/src/libs/ffmpeg-executor.ts b/src/libs/ffmpeg-executor.ts
index 2357bb9..6926257 100644
--- a/src/libs/ffmpeg-executor.ts
+++ b/src/libs/ffmpeg-executor.ts
@@ -1,6 +1,10 @@
-import { log } from "@clack/prompts";
 import { spawn } from "child_process";
 import { parseProgressLine } from "../core/ffmpeg/progress-parser.js";
+import {
+  FFmpegProcessError,
+  FFprobeProcessError,
+  SpawnProcessError,
+} from "../error.js";
 import type { ProgressInfo } from "../types.js";
 
 function spawnFFmpegProcess(
@@ -25,7 +29,7 @@ function spawnFFmpegProcess(
 
     const handleError = (error: Error) => {
       if (!child.killed) child.kill();
-      reject(Object.assign(error, { out, err }));
+      reject(new SpawnProcessError(error.message, err, error));
     };
 
     /**
@@ -72,10 +76,11 @@ function spawnFFmpegProcess(
       if (code === 0) {
         resolve({ out, err });
       } else {
-        const error = new Error(
-          `FFmpeg exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
+        handleError(
+          new Error(
+            `FFmpeg exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
+          ),
         );
-        handleError(Object.assign(error, { code, signal, out, err }));
       }
     });
 
@@ -108,33 +113,30 @@ function spawnFFprobeProcess(
       windowsHide: true,
       signal,
     });
-
     const handleError = (error: Error) => {
       if (!child.killed) child.kill();
-      reject(Object.assign(error, { out, err }));
+      reject(new SpawnProcessError(error.message, err, error));
     };
-
     signal?.addEventListener("abort", () => {
       if (!child.killed) child.kill("SIGKILL");
     });
-
     child.stdout.setEncoding("utf8");
     child.stderr.setEncoding("utf8");
-
     child.on("error", handleError);
-
-    child.on("close", (code) => {
+    child.on("close", (code, signal) => {
       if (code !== 0) {
-        reject(new Error(`ffprobe exited with code ${code}: ${err}`));
+        handleError(
+          new Error(
+            `FFprobe exited with code ${code}${signal ? ` (signal: ${signal})` : ""}`,
+          ),
+        );
       } else {
         resolve({ out, err });
       }
     });
-
     child.stdout.on("data", (data: string) => {
       out += data;
     });
-
     child.stderr.on("data", (data: string) => {
       err += data;
     });
@@ -146,10 +148,11 @@ export async function runFFprobeCommand(args: string[]) {
   try {
     return await spawnFFprobeProcess(args, ac.signal);
   } catch (error) {
-    if (error instanceof Error) {
-      log.error(error.message);
+    // TODO: report to console with a better way
+    if (error instanceof SpawnProcessError) {
+      throw new FFprobeProcessError("ffprobe process error", error);
     } else {
-      console.error(error);
+      throw error;
     }
   } finally {
     ac.abort();
@@ -164,10 +167,11 @@ export async function runFFmpegCommand(
   try {
     return await spawnFFmpegProcess(args, ac.signal, onProgress);
   } catch (error) {
-    if (error instanceof Error) {
-      log.error(error.message);
+    // TODO: report to console with a better way
+    if (error instanceof SpawnProcessError) {
+      throw new FFmpegProcessError("ffmpeg process error", error);
     } else {
-      console.error(error);
+      throw error;
     }
   } finally {
     ac.abort();
diff --git a/src/types.ts b/src/types.ts
index 428f15b..3195164 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -55,13 +55,13 @@ export interface ProgressInfo {
   progress?: "continue" | "end";
 }
 
-export interface VideoInfo {
+export interface ImageInfo {
   input: string;
-  metadata: FFprobeResultConvertdResult;
 }
 
-export interface ImageInfo {
+export interface VideoInfo {
   input: string;
+  metadata: FFprobeResultConvertdResult;
 }
 
 export interface FFprobeResultConvertdResult {
@@ -69,9 +69,11 @@ export interface FFprobeResultConvertdResult {
   codec_tag_string: string;
   width: number;
   height: number;
-  avg_frame_rate: number;
+  pix_fmt: string;
+  // avg_frame_rate: number;
   duration: number;
-  bit_rate: number;
+  // bit_rate: number;
+  bits_per_raw_sample: number;
 }
 
 export type TaskType = (typeof TASK_TYPE)[number];
diff --git a/src/utils/basic-convert.ts b/src/utils/basic-convert.ts
new file mode 100644
index 0000000..86f2bc0
--- /dev/null
+++ b/src/utils/basic-convert.ts
@@ -0,0 +1,13 @@
+/**
+ * try convert string to number
+ * @example
+ * tryConvertStringToNumber("100") ?? 10
+ * @param value string to be convert
+ * @returns converted number or undefined
+ */
+export function tryConvertStringToNumber(value: string): number | undefined {
+  const trimmed = value.trim();
+  if (trimmed === "") return undefined;
+  const num = Number(trimmed);
+  return Number.isNaN(num) ? undefined : num;
+}
diff --git a/src/utils/test-utils.ts b/src/utils/test-utils.ts
new file mode 100644
index 0000000..6ab14e9
--- /dev/null
+++ b/src/utils/test-utils.ts
@@ -0,0 +1,41 @@
+import type { FFprobeResultConvertdResult, VideoInfo } from "../types.js";
+
+export function createVideoInfo(
+  config: {
+    input?: VideoInfo["input"];
+    metadata?: Partial<VideoInfo["metadata"]>;
+  } = {},
+): VideoInfo {
+  const DEFAULT_CONFIG: VideoInfo = {
+    input: "input.mp4",
+    metadata: createFFprobeResultConvertdResult(),
+  };
+
+  return {
+    ...DEFAULT_CONFIG,
+    ...config,
+    metadata: {
+      ...DEFAULT_CONFIG.metadata,
+      ...config.metadata,
+    },
+  };
+}
+
+export function createFFprobeResultConvertdResult(
+  result: Partial<FFprobeResultConvertdResult> = {},
+): FFprobeResultConvertdResult {
+  const DEFAULT_RESULT: FFprobeResultConvertdResult = {
+    codec_name: "hevc",
+    codec_tag_string: "hev1",
+    width: 1920,
+    height: 1080,
+    duration: 100,
+    pix_fmt: "yuv420p",
+    bits_per_raw_sample: 8,
+  };
+
+  return {
+    ...DEFAULT_RESULT,
+    ...result,
+  };
+}


Reviews:
user: login: coderabbitai[bot]
body: **Actionable comments posted: 5**

<details>
<summary>🧹 Nitpick comments (16)</summary><blockquote>

<details>
<summary>.changeset/cool-eels-fry.md (1)</summary><blockquote>

`5-5`: **Minor grammar improvement needed in changeset message.**

The changeset message should include an article for better readability.

```diff
-support bit depth encoding
+support for bit depth encoding
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 LanguageTool</summary>

[grammar] ~5-~5: It seems that an article is missing.
Context: ...obforreal/clitools": minor ---  support bit depth encoding 

(A_BIT)

</details>

</details>

</blockquote></details>
<details>
<summary>src/core/image/encode.ts (1)</summary><blockquote>

`56-60`: **Redundancy in error type checking.**

The condition `error instanceof NotSupportedError || error instanceof Error` is redundant since `NotSupportedError` extends `Error`. The first check is unnecessary as the second would catch it anyway.

```diff
-    if (error instanceof NotSupportedError || error instanceof Error) {
+    if (error instanceof Error) {
       throw new EncodeImageError(error.message, error);
     } else {
       throw error;
     }
```

</blockquote></details>
<details>
<summary>src/types.ts (2)</summary><blockquote>

`58-65`: **Swapped interface names may cause confusion**

The interfaces `VideoInfo` and `ImageInfo` appear to have been swapped in their declarations. While this might align with your implementation, it could be confusing for developers familiar with the previous structure.

Consider adding documentation comments explaining this change to guide future developers.

---

`67-77`: **Commented out properties in interface**

Rather than commenting out `avg_frame_rate` and `bit_rate`, consider either:
1. Removing them completely if they're no longer needed
2. Making them optional with `?` if they might be used in some contexts
3. Adding a comment explaining why they're commented out

Additionally, the interface name `FFprobeResultConvertdResult` appears to have a typo (missing 'e' in "Converted").


```diff
-export interface FFprobeResultConvertdResult {
+export interface FFprobeResultConvertedResult {
  codec_name: string;
  codec_tag_string: string;
  width: number;
  height: number;
  pix_fmt: string;
-  // avg_frame_rate: number;
+  avg_frame_rate?: number; // Optional but might be needed in some contexts
  duration: number;
-  // bit_rate: number;
+  bit_rate?: number; // Optional but might be needed in some contexts
  bits_per_raw_sample: number;
}
```

</blockquote></details>
<details>
<summary>src/core/video/pipeline.test.ts (1)</summary><blockquote>

`106-181`: **Comprehensive test coverage for new functionality.**

The new test suite for `filterHev1Video` covers all important scenarios including matching videos, no matches, empty input, and strict matching requirements. The use of `createVideoInfo` utility makes the tests more readable and maintainable.


You might want to remove the commented-out lines (133-137, 156-157) as they don't add value to the tests.


```diff
-      // createVideoInfo("hevc", "hev1"),
-      // createVideoInfo("hevc", "hvc1"),
-      // createVideoInfo("avc1", "hev1"),
-      // createVideoInfo("avc1", "avc1"),
```

</blockquote></details>
<details>
<summary>src/core/video/metadata.ts (1)</summary><blockquote>

`80-83`: **Document the default value choice for bits_per_raw_sample.**

The code uses 8 as a default value when the conversion fails or the value is missing, which is a sensible default for 8-bit video. However, it would be helpful to add a comment explaining this choice.


```diff
 case "bits_per_raw_sample": {
+  // Default to 8 bits per sample if conversion fails, as it's the most common bit depth
   resultObject[key] = tryConvertStringToNumber(value) ?? 8;
   requiredFields.delete(key);
```

</blockquote></details>
<details>
<summary>src/features/encode-video/index.ts (2)</summary><blockquote>

`10-18`: **Function renaming improves clarity and task delegation pattern looks good.**

The rename from `videoEncodeTask` to `encodeVideoTask` is more consistent with action-first naming conventions. The extraction of encoding logic to a separate function improves modularity.


One minor optimization:
```diff
  for (let index = 0; index < videoInfoList.length; index++) {
    const videoInfo = videoInfoList[index];
-   if (videoInfo !== undefined) {
      await encodeVideo(videoInfo, index, videoInfoList.length);
-   }
  }
```

The null check isn't necessary since you're iterating through an array returned by `getVideoInfoListFromUserInput`, which would throw an error if no videos were found.

---

`20-47`: **Good implementation of progress feedback with error handling.**

The spinner implementation provides excellent user feedback during encoding operations. Error handling correctly distinguishes between FFmpeg-specific errors and other errors.


Consider enhancing the error handling for unknown errors:
```diff
    } else {
-     errMessage += "unknown error";
+     errMessage += error instanceof Error ? error.message : String(error);
    }
```

This would provide more context when non-FFmpeg errors occur.

</blockquote></details>
<details>
<summary>src/features/enable-quickTime/index.ts (1)</summary><blockquote>

`12-26`: **Excellent use of new filtering utility and error handling.**

Using the `filterHev1Video` utility and throwing a specific `NothingToProcessError` when no applicable videos are found improves code clarity and error specificity.


One minor optimization:
```diff
  for (let index = 0; index < filteredVideos.length; index++) {
    const videoInfo = filteredVideos[index];
-   if (videoInfo !== undefined) {
      await enableHEVCQuickTime(videoInfo, index, filteredVideos.length);
-   }
  }
```

The null check isn't necessary since you're iterating through a filtered array.

</blockquote></details>
<details>
<summary>src/libs/ffmpeg-executor.ts (1)</summary><blockquote>

`170-175`: **Consistent error handling pattern applied to FFmpeg command.**

Good use of the same error handling pattern as in FFprobe command for consistency.


The TODO comment here should also be addressed.

</blockquote></details>
<details>
<summary>src/core/video/pipeline.ts (2)</summary><blockquote>

`66-68`: **Good use of NothingToProcessError.**

Using a specialized error class with a descriptive message clarifies the error condition.


Consider including the path in the error message for better context:
```diff
  if (collectedVideoPaths.length === 0) {
-   throw new NothingToProcessError("No supported video files found");
+   throw new NothingToProcessError(`No supported video files found in "${normalizedPath}"`);
  }
```

---

`78-84`: **Well-implemented filter utility function.**

The `filterHev1Video` function is a good extraction of reusable logic for filtering videos with specific codec properties.


Consider adding null/undefined checks for robustness:
```diff
  return videoInfoList.filter(
    (v) =>
-     v.metadata.codec_name === "hevc" &&
-     v.metadata.codec_tag_string === "hev1",
+     v.metadata?.codec_name === "hevc" &&
+     v.metadata?.codec_tag_string === "hev1",
  );
```

</blockquote></details>
<details>
<summary>src/error.ts (2)</summary><blockquote>

`8-24`: **Redundant `cause` property – leverage built-in Error#cause instead**

Both `SpawnProcessError` and `FFmpegProcessError` manually introduce a `cause` field, duplicating the standard ES 2022 `Error` option.  TypeScript already exposes this via the second parameter of `super(message, { cause })`.

A lighter implementation:

```diff
-  public override cause: Error;
-  constructor(message: string, stderr: string, cause: Error) {
-    super(message);
-    this.cause = cause;
+  constructor(message: string, stderr: string, cause: Error) {
+    super(message, { cause });
     this.stderr = stderr;
   }
```

Repeat the same simplification for `FFmpegProcessError` & `FFprobeProcessError`.

---

`60-64`: **Unnecessary boiler-plate constructors flagged by linter**

`ExtractVideoMetadataError`, `MissingBinariesError`, `NotSupportedError`, and `NothingToProcessError` add no new properties but still redeclare the constructor.  The base `MyError` already handles the message/​name.

```diff
-export class ExtractVideoMetadataError extends MyError {
-  constructor(message: string) {
-    super(message);
-  }
-}
+export class ExtractVideoMetadataError extends MyError {}
```

Apply the same reduction to the three other classes to comply with `noUselessConstructor` and keep the file concise.



Also applies to: 74-78, 80-84, 86-90

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 61-63: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

</details>

</details>

</blockquote></details>
<details>
<summary>src/core/ffmpeg/command-builder.ts (2)</summary><blockquote>

`127-133`: **Risk of forgotten `applyX265Params()` call**

`x265Params` are only appended if the caller explicitly invokes `applyX265Params()`.  
Forgetting this extra step silently drops all accumulated parameters.

Consider automatically appending in `build()` when `x265Params.length > 0`, eliminating the foot-gun:

```diff
   build(): string[] {
-    return [
+    if (this instanceof FFmpegH265CommandBuilder) {
+      (this as FFmpegH265CommandBuilder).applyX265Params();
+    }
+    return [
       ...this.globalOptions,
       ...this.inputs,
       ...this.outputOptions,
       this.outputFile,
     ];
   }
```

---

`92-96`: **Silently ignoring `setVideoEncoder()` may hide bugs**

Overriding `setVideoEncoder` to a no-op prevents consumers from changing the encoder (good) but provides no feedback.  
Throwing a purposeful error improves developer experience and prevents silent mis-configuration:

```diff
-  override setVideoEncoder(_encode: string): this {
-    return this;
+  override setVideoEncoder(_encode: string): this {
+    throw new NotSupportedError(
+      "FFmpegH265CommandBuilder uses a fixed encoder (libx265); changing it is not supported."
+    );
   }
```

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 08e5306e87b0942854e0337cfed00127e2ac8ab0 and bd1bfad26bb57d7fd3e79146bb1c5c1e95b07504.

</details>

<details>
<summary>📒 Files selected for processing (27)</summary>

* `.changeset/cool-eels-fry.md` (1 hunks)
* `.github/workflows/sync-metadata.yml` (0 hunks)
* `fixtures/ffprobe output.txt` (2 hunks)
* `src/core/ffmpeg/checker.ts` (2 hunks)
* `src/core/ffmpeg/command-builder.ts` (4 hunks)
* `src/core/image/encode.ts` (1 hunks)
* `src/core/image/pipeline.ts` (2 hunks)
* `src/core/video/metadata.test.ts` (3 hunks)
* `src/core/video/metadata.ts` (3 hunks)
* `src/core/video/pipeline.test.ts` (4 hunks)
* `src/core/video/pipeline.ts` (4 hunks)
* `src/error.ts` (1 hunks)
* `src/features/cli/index.test.ts` (5 hunks)
* `src/features/cli/index.ts` (1 hunks)
* `src/features/cli/progress-handler.test.ts` (1 hunks)
* `src/features/cli/progress-handler.ts` (2 hunks)
* `src/features/enable-quickTime/args.test.ts` (2 hunks)
* `src/features/enable-quickTime/index.ts` (1 hunks)
* `src/features/encode-image/index.ts` (1 hunks)
* `src/features/encode-video/args.test.ts` (2 hunks)
* `src/features/encode-video/args.ts` (2 hunks)
* `src/features/encode-video/index.ts` (1 hunks)
* `src/index.ts` (2 hunks)
* `src/libs/ffmpeg-executor.ts` (6 hunks)
* `src/types.ts` (1 hunks)
* `src/utils/basic-convert.ts` (1 hunks)
* `src/utils/test-utils.ts` (1 hunks)

</details>

<details>
<summary>💤 Files with no reviewable changes (1)</summary>

* .github/workflows/sync-metadata.yml

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🧬 Code Graph Analysis (16)</summary>

<details>
<summary>src/features/enable-quickTime/args.test.ts (2)</summary><blockquote>

<details>
<summary>src/utils/test-utils.ts (1)</summary>

* `createVideoInfo` (3-22)

</details>
<details>
<summary>src/features/enable-quickTime/args.ts (1)</summary>

* `buildHEVCEnableQuickTimeArgs` (5-21)

</details>

</blockquote></details>
<details>
<summary>src/features/encode-image/index.ts (4)</summary><blockquote>

<details>
<summary>src/core/image/pipeline.ts (1)</summary>

* `getImageListFromUserInput` (6-18)

</details>
<details>
<summary>src/libs/concurrency.ts (1)</summary>

* `createConcurrencyLimit` (4-6)

</details>
<details>
<summary>src/core/image/encode.ts (1)</summary>

* `encodeImage` (11-62)

</details>
<details>
<summary>src/error.ts (1)</summary>

* `EncodeImageError` (52-58)

</details>

</blockquote></details>
<details>
<summary>src/index.ts (1)</summary><blockquote>

<details>
<summary>src/error.ts (1)</summary>

* `NothingToProcessError` (86-90)

</details>

</blockquote></details>
<details>
<summary>src/core/ffmpeg/checker.ts (1)</summary><blockquote>

<details>
<summary>src/error.ts (1)</summary>

* `MissingBinariesError` (74-78)

</details>

</blockquote></details>
<details>
<summary>src/features/cli/index.test.ts (7)</summary><blockquote>

<details>
<summary>src/features/encode-video/index.ts (1)</summary>

* `encodeVideoTask` (10-18)

</details>
<details>
<summary>src/features/enable-quickTime/index.ts (1)</summary>

* `enableHEVCQuickTimeTask` (12-26)

</details>
<details>
<summary>src/features/encode-image/index.ts (1)</summary>

* `encodeImageTask` (8-39)

</details>
<details>
<summary>src/core/ffmpeg/checker.ts (1)</summary>

* `checkFFmpegInstallation` (5-47)

</details>
<details>
<summary>src/features/cli/task-detail.ts (1)</summary>

* `getTaskDetail` (8-25)

</details>
<details>
<summary>src/libs/prompt.ts (1)</summary>

* `askForContinue` (93-102)

</details>
<details>
<summary>src/features/cli/index.ts (1)</summary>

* `runCli` (17-26)

</details>

</blockquote></details>
<details>
<summary>src/features/cli/progress-handler.ts (1)</summary><blockquote>

<details>
<summary>src/types.ts (1)</summary>

* `ProgressInfo` (42-56)

</details>

</blockquote></details>
<details>
<summary>src/core/image/pipeline.ts (1)</summary><blockquote>

<details>
<summary>src/error.ts (1)</summary>

* `NothingToProcessError` (86-90)

</details>

</blockquote></details>
<details>
<summary>src/utils/test-utils.ts (1)</summary><blockquote>

<details>
<summary>src/types.ts (2)</summary>

* `VideoInfo` (62-65)
* `FFprobeResultConvertdResult` (67-77)

</details>

</blockquote></details>
<details>
<summary>src/features/cli/index.ts (6)</summary><blockquote>

<details>
<summary>src/features/encode-video/index.ts (1)</summary>

* `encodeVideoTask` (10-18)

</details>
<details>
<summary>src/features/enable-quickTime/index.ts (1)</summary>

* `enableHEVCQuickTimeTask` (12-26)

</details>
<details>
<summary>src/features/encode-image/index.ts (1)</summary>

* `encodeImageTask` (8-39)

</details>
<details>
<summary>src/features/cli/task-detail.ts (1)</summary>

* `getTaskDetail` (8-25)

</details>
<details>
<summary>src/libs/prompt.ts (1)</summary>

* `askForContinue` (93-102)

</details>
<details>
<summary>src/core/ffmpeg/checker.ts (1)</summary>

* `checkFFmpegInstallation` (5-47)

</details>

</blockquote></details>
<details>
<summary>src/features/enable-quickTime/index.ts (6)</summary><blockquote>

<details>
<summary>src/types.ts (2)</summary>

* `EnableHEVCQuickTimeTaskProps` (39-39)
* `VideoInfo` (62-65)

</details>
<details>
<summary>src/core/video/pipeline.ts (2)</summary>

* `getVideoInfoListFromUserInput` (59-76)
* `filterHev1Video` (78-84)

</details>
<details>
<summary>src/error.ts (2)</summary>

* `NothingToProcessError` (86-90)
* `FFmpegProcessError` (18-24)

</details>
<details>
<summary>src/utils/path.ts (1)</summary>

* `getFileNameFromPath` (12-17)

</details>
<details>
<summary>src/libs/ffmpeg-executor.ts (1)</summary>

* `runFFmpegCommand` (162-179)

</details>
<details>
<summary>src/features/enable-quickTime/args.ts (1)</summary>

* `buildHEVCEnableQuickTimeArgs` (5-21)

</details>

</blockquote></details>
<details>
<summary>src/features/encode-video/args.ts (3)</summary><blockquote>

<details>
<summary>src/core/video/crf-calculation.ts (1)</summary>

* `calculateCrfByPixelCount` (8-17)

</details>
<details>
<summary>src/utils/output-generator.ts (1)</summary>

* `generateOutputPath` (14-19)

</details>
<details>
<summary>src/core/ffmpeg/command-builder.ts (1)</summary>

* `FFmpegH265CommandBuilder` (72-133)

</details>

</blockquote></details>
<details>
<summary>src/features/encode-video/index.ts (6)</summary><blockquote>

<details>
<summary>src/types.ts (2)</summary>

* `EncodeVideoTaskProps` (35-37)
* `VideoInfo` (62-65)

</details>
<details>
<summary>src/core/video/pipeline.ts (1)</summary>

* `getVideoInfoListFromUserInput` (59-76)

</details>
<details>
<summary>src/utils/path.ts (1)</summary>

* `getFileNameFromPath` (12-17)

</details>
<details>
<summary>src/libs/ffmpeg-executor.ts (1)</summary>

* `runFFmpegCommand` (162-179)

</details>
<details>
<summary>src/features/cli/progress-handler.ts (1)</summary>

* `createProgressHandler` (4-43)

</details>
<details>
<summary>src/error.ts (1)</summary>

* `FFmpegProcessError` (18-24)

</details>

</blockquote></details>
<details>
<summary>src/core/video/metadata.ts (4)</summary><blockquote>

<details>
<summary>src/types.ts (1)</summary>

* `FFprobeResultConvertdResult` (67-77)

</details>
<details>
<summary>src/libs/ffmpeg-executor.ts (1)</summary>

* `runFFprobeCommand` (146-160)

</details>
<details>
<summary>src/error.ts (4)</summary>

* `FFprobeProcessError` (26-26)
* `KVError` (28-34)
* `StringToNumberCovertError` (36-42)
* `RequiredFieldError` (66-72)

</details>
<details>
<summary>src/utils/basic-convert.ts (1)</summary>

* `tryConvertStringToNumber` (8-13)

</details>

</blockquote></details>
<details>
<summary>src/core/video/pipeline.test.ts (3)</summary><blockquote>

<details>
<summary>src/utils/test-utils.ts (2)</summary>

* `createFFprobeResultConvertdResult` (24-41)
* `createVideoInfo` (3-22)

</details>
<details>
<summary>src/error.ts (1)</summary>

* `NothingToProcessError` (86-90)

</details>
<details>
<summary>src/core/video/pipeline.ts (1)</summary>

* `filterHev1Video` (78-84)

</details>

</blockquote></details>
<details>
<summary>src/libs/ffmpeg-executor.ts (1)</summary><blockquote>

<details>
<summary>src/error.ts (3)</summary>

* `SpawnProcessError` (8-16)
* `FFprobeProcessError` (26-26)
* `FFmpegProcessError` (18-24)

</details>

</blockquote></details>
<details>
<summary>src/core/video/pipeline.ts (2)</summary><blockquote>

<details>
<summary>src/error.ts (2)</summary>

* `ExtractVideoMetadataError` (60-64)
* `NothingToProcessError` (86-90)

</details>
<details>
<summary>src/types.ts (1)</summary>

* `VideoInfo` (62-65)

</details>

</blockquote></details>

</details><details>
<summary>🪛 LanguageTool</summary>

<details>
<summary>.changeset/cool-eels-fry.md</summary>

[grammar] ~5-~5: It seems that an article is missing.
Context: ...obforreal/clitools": minor ---  support bit depth encoding 

(A_BIT)

</details>

</details>
<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/error.ts</summary>

[error] 61-63: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 75-77: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 81-83: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 87-89: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (67)</summary><blockquote>

<details>
<summary>fixtures/ffprobe output.txt (2)</summary>

`25-26`: **Appropriate formatting update for metadata fields.**

The added blank lines after `display_aspect_ratio` and before `pix_fmt` field align with the enhanced metadata extraction and validation logic introduced in the PR. This formatting change helps make the output more readable and properly structured.

---

`47-49`: **Appropriate formatting update for bit depth metadata fields.**

The added blank lines around the `bits_per_raw_sample` field align with the code changes in related modules that now extract and handle bit depth information. This update ensures the test fixture properly represents the expected format.

</details>
<details>
<summary>src/utils/basic-convert.ts (1)</summary>

`1-13`: **Well-implemented utility function for safer number conversion.**

This utility function properly handles string-to-number conversion with appropriate edge cases:
- Returns `undefined` instead of `NaN` for empty or invalid strings
- Trims input to handle whitespace
- Includes clear JSDoc with example usage

This is a good pattern for safer type conversion that will help prevent NaN-related bugs throughout the codebase.

</details>
<details>
<summary>src/core/image/pipeline.ts (2)</summary>

`1-1`: **Good use of specialized error class for better error handling.**

Importing and using the custom `NothingToProcessError` class aligns with the broader improvements to error handling across the codebase. This provides more specific error types that can be explicitly caught and handled.

---

`13-15`: **Consider addressing the TODO comment.**

There's a TODO comment indicating a need for a "scan error class". Since you're already improving error handling in this PR, consider addressing this TODO by implementing the scan error class as mentioned.

Is this TODO planned to be addressed in a future PR? Or should it be addressed as part of this error handling refactoring effort?

</details>
<details>
<summary>src/core/ffmpeg/checker.ts (2)</summary>

`3-3`: **Good use of specific error type**

Importing and using the `MissingBinariesError` class is a good practice that aligns with the project's move toward standardized error handling.

---

`45-45`: **Well-implemented error type replacement**

Replacing the generic `Error` with the specific `MissingBinariesError` improves error handling by making the error more identifiable and allows for more targeted error handling in the catching code.

</details>
<details>
<summary>src/features/enable-quickTime/args.test.ts (3)</summary>

`2-2`: **Good use of test utility**

Using the centralized test utility will help maintain consistency across test files.

---

`14-15`: **Improved test setup**

Using the `createVideoInfo()` utility function instead of manually constructing test data improves maintainability and consistency.

---

`17-23`: **Better test assertions**

Breaking down the assertion into multiple regex-based checks is a significant improvement over a single snapshot test. This makes the test:
1. More granular and easier to debug
2. More resilient to unrelated changes in the command
3. Better at verifying specific components of the command

</details>
<details>
<summary>src/index.ts (3)</summary>

`3-4`: **Good imports for improved error handling**

Using `log` from `@clack/prompts` ensures consistent error formatting, and importing the specific `NothingToProcessError` aligns with the project's improved error handling approach.

---

`20-20`: **Consistent error formatting**

Replacing `console.log` with `log.error` ensures consistent error output formatting throughout the application.

---

`22-24`: **Good specific error handling**

Adding a dedicated handler for `NothingToProcessError` improves the error reporting experience for users when no files are found to process.

</details>
<details>
<summary>src/features/cli/index.ts (3)</summary>

`1-1`: **Good imports with updated naming conventions**

The imports follow the project's updated naming conventions, with task functions renamed to be more descriptive (`encodeImageTask` and `encodeVideoTask`).



Also applies to: 4-5

---

`8-15`: **Excellent refactoring with taskHandlers object**

Using a declarative `taskHandlers` object to map tasks to their configurations is a significant improvement:
1. Eliminates repetitive code in the switch statement
2. Makes it easy to add new tasks in the future
3. Centralizes task configuration in one place
4. Clearly documents which tasks require FFmpeg

---

`23-25`: **Clean task execution logic**

The refactored task execution logic is more maintainable:
1. Conditionally checks FFmpeg installation only when needed
2. Consistently handles all tasks with the same pattern
3. Reduces code duplication

</details>
<details>
<summary>src/core/image/encode.ts (2)</summary>

`5-5`: **Improved error handling with custom error types.**

The introduction of specific error types enhances error handling clarity and consistency across the codebase.

---

`13-54`: **Well-structured image encoding implementation with improved flow.**

The refactored code effectively handles different image formats with appropriate sharp processors and uses async/await for better readability.

</details>
<details>
<summary>src/features/encode-image/index.ts (3)</summary>

`8-8`: **Function renamed for consistency.**

Renaming from `imageEncodeTask` to `encodeImageTask` improves naming consistency across the codebase.

---

`14-14`: **Simplified task creation.**

Removing the inline error handling in favor of centralized error processing after all promises settle is a cleaner approach.

---

`19-28`: **Improved error classification with custom error types.**

Using `instanceof EncodeImageError` for more specific error classification provides better error reporting to users.

</details>
<details>
<summary>src/features/encode-video/args.ts (3)</summary>

`1-1`: **Specialized H.265 command builder enhances encoding capabilities.**

Using the specialized `FFmpegH265CommandBuilder` instead of the generic command builder allows for better H.265-specific encoding options.

---

`10-10`: **Enhanced metadata extraction for better encoding.**

Extracting additional metadata properties `pix_fmt` and `bits_per_raw_sample` enables more precise video encoding configuration.

---

`16-22`: **Improved H.265 encoding configuration.**

The sequence of method calls specific to H.265 encoding (setting log level, profile, bit depth, etc.) provides better control over the encoding process and output quality.

</details>
<details>
<summary>src/features/cli/progress-handler.test.ts (5)</summary>

`5-14`: **Well-structured test setup with proper mocking.**

The test suite follows good practices with clear setup, mocking, and organization.

---

`15-27`: **Comprehensive tests for edge cases.**

Tests properly verify handling of the "end" event and cases with missing critical fields.

---

`29-45`: **Well-implemented throttling tests.**

Effective use of fake timers to test throttling behavior ensures progress updates don't overwhelm the user interface.

---

`47-61`: **Thorough validation of calculation logic.**

Tests verify that percentage calculation has proper upper limits and that ETA calculation prevents negative values.

---

`63-79`: **Detailed format verification.**

Tests ensure progress messages are formatted with correct padding for readability.

</details>
<details>
<summary>src/features/encode-video/args.test.ts (3)</summary>

`2-2`: **Good use of test utilities for better maintainability**

Using `createVideoInfo()` utility function improves test maintainability by centralizing test fixture creation.

---

`13-29`: **Improved test assertions with better granularity**

The refactoring from a single inline snapshot to multiple regex-based assertions is excellent. This approach:
1. Makes test failures more specific and easier to debug
2. Improves readability by clearly showing what's being tested
3. Makes the tests more resilient to minor changes in the output format

---

`31-58`: **Good coverage for pixel format handling**

This new test case properly verifies that different pixel formats map to the correct profiles in the x265 parameters. Testing these mappings is critical as they affect encoding quality and compatibility.

</details>
<details>
<summary>src/features/cli/index.test.ts (5)</summary>

`2-12`: **Good addition of FFmpeg installation check mock**

Adding the mock for `checkFFmpegInstallation` allows proper testing of the error handling path when FFmpeg is not installed. This is critical for CLI applications that depend on external tools.

---

`18-24`: **Function naming consistency**

Good update of mock function names from `videoEncodeTask` to `encodeVideoTask` and `imageEncodeTask` to `encodeImageTask`. This maintains consistency with the implementation and makes tests easier to understand.

---

`53-67`: **Good test for FFmpeg check before task execution**

This test properly verifies that the FFmpeg installation check happens before executing the video encoding task. This ensures the application fails early with a clear error message when requirements aren't met.

---

`69-85`: **Thorough error handling test**

Excellent test case for verifying error handling when FFmpeg check fails. This covers an important failure path and ensures the task isn't executed when prerequisites aren't met.

---

`103-119`: **Complete test coverage for error handling**

Good duplication of the error handling test for the HEVC QuickTime task. This ensures consistent behavior across different task types and prevents regression issues.

</details>
<details>
<summary>src/features/cli/progress-handler.ts (4)</summary>

`4-9`: **Good dependency injection pattern**

Adding the `messageHandler` parameter is a great improvement that:
1. Makes the function more testable
2. Decouples the progress handling from the specific UI implementation
3. Follows good dependency injection principles

---

`14-15`: **Improved robustness with undefined checks**

Good addition of explicit checks for undefined values in the progress object. This prevents potential runtime errors when handling incomplete progress data.

---

`17-20`: **Clear handling of completion event**

Good implementation of the "end" progress event with a fixed 100% completion message and zero ETA. This provides a clean user experience when the process completes.

---

`39-41`: **Consistent progress message formatting**

The progress message formatting with percentage padding and fixed decimal places provides a clean and consistent user interface. Consider adding a unit test to verify the formatting remains correct.

</details>
<details>
<summary>src/utils/test-utils.ts (2)</summary>

`3-22`: **Great utility function implementation!**

The `createVideoInfo` function is well-designed with proper type signatures and effective use of default values with partial override support. The nested merge for the metadata field ensures that metadata overrides are correctly applied.

---

`24-41`: **Well-structured test data factory function.**

The `createFFprobeResultConvertdResult` function provides a clean way to generate consistent test data with sensible defaults for HEVC/H.265 video properties. This will help maintain consistency across test cases and reduce duplication.

</details>
<details>
<summary>src/core/video/pipeline.test.ts (3)</summary>

`4-10`: **Good use of test utilities and improved error handling.**

The changes make effective use of the new test utilities and custom error types, which improves test reliability and maintainability. Using `createFFprobeResultConvertdResult()` for mock data ensures consistency across tests.



Also applies to: 53-53

---

`79-84`: **Improved error expectation.**

The test now correctly expects a specific `NothingToProcessError` type rather than a generic string match, which aligns with the custom error handling approach introduced in the codebase.

---

`94-103`: **Better error handling test.**

Changing the test to expect a rejection with a specific error type rather than checking console output is a more direct and reliable testing approach.

</details>
<details>
<summary>src/core/video/metadata.test.ts (4)</summary>

`2-4`: **Good imports for error types and test utilities.**

Importing specific error classes and the test utility function improves code clarity and ensures consistent error handling throughout the tests.

---

`26-37`: **Updated test data with improved verification.**

The test data now includes the new fields `pix_fmt` and `bits_per_raw_sample`, and the test uses `createFFprobeResultConvertdResult()` for expected results, which ensures consistency with other tests.

---

`56-68`: **Good edge case testing.**

Adding a test for handling lines with empty values ensures the parser is robust against different input formats that may occur in real-world scenarios.

---

`122-144`: **Improved error handling in tests.**

The tests now expect specific error types (`FFprobeProcessError`) instead of generic errors, which aligns with the improved error handling strategy in the codebase.

</details>
<details>
<summary>src/core/video/metadata.ts (5)</summary>

`1-9`: **Good use of specific error types.**

Importing specific error classes from a centralized module improves error handling consistency and makes it easier to understand and handle different error scenarios.

---

`16-21`: **Improved error handling for empty output.**

Throwing a specific `FFprobeProcessError` with the error message from ffprobe provides better context for debugging and aligns with the centralized error handling strategy.

---

`31-41`: **Updated required fields for metadata.**

The required fields now include `pix_fmt` and `bits_per_raw_sample` while disabling `avg_frame_rate` and `bit_rate`, which reflects the current needs of the application.

---

`43-57`: **Better error handling in parsing functions.**

Using specific error types like `KVError` and `StringToNumberCovertError` instead of generic errors provides more context for debugging and aligns with the centralized error handling strategy.

---

`93-97`: **Improved error reporting for missing fields.**

Using `RequiredFieldError` with a list of missing fields provides better context for debugging and makes it easier to identify which fields are missing.

</details>
<details>
<summary>src/features/encode-video/index.ts (1)</summary>

`1-1`: **Import organization is well-structured.**

Good job on importing specific error types from the centralized error module and utilities as needed. This helps with maintainability and aligns with the error handling strategy across the codebase.



Also applies to: 3-3, 6-6

</details>
<details>
<summary>src/features/enable-quickTime/index.ts (1)</summary>

`1-10`: **Good modularization with appropriate imports.**

The imports are well organized, and the use of specific error types from the centralized error module aligns with the project's error handling strategy.

</details>
<details>
<summary>src/libs/ffmpeg-executor.ts (6)</summary>

`3-7`: **Good refactoring to use specialized error classes.**

The introduction of specific error classes improves error handling and aligns with good OOP principles, separating concerns and providing better context for error reporting.

---

`32-33`: **Improved error handling with SpawnProcessError.**

Good enhancement of error handling by using a specialized error class that includes stderr output.

---

`79-83`: **Better process exit error reporting.**

The addition of signal information to the error message provides valuable context for debugging process termination issues.

---

`118-119`: **Consistent error handling in FFprobe process.**

Good application of the same error handling pattern as in FFmpeg process.

---

`126-132`: **Signal information added to FFprobe process errors.**

Good enhancement by including signal information in FFprobe error messages for better debugging.

---

`151-156`: **TODO comments should be addressed.**

There's a TODO comment about reporting to the console in a better way, but no clear implementation path.


Can you clarify the plan for addressing this TODO? What's the intended approach for console reporting?

</details>
<details>
<summary>src/core/video/pipeline.ts (4)</summary>

`3-6`: **Good use of specialized error types.**

Importing specific error classes from the centralized error module improves error handling consistency throughout the codebase.

---

`21-22`: **Improved error specificity with ExtractVideoMetadataError.**

Using a specialized error class for metadata extraction failures provides better context for error handling.

---

`28-33`: **Good error normalization.**

The error normalization is well implemented, distinguishing between different error types and adding context.

---

`70-74`: **Clear error message for videos without valid metadata.**

Good error handling for cases where files are found but metadata couldn't be extracted.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T07:32:46Z
user: login: coderabbitai[bot]
body: **Actionable comments posted: 0**

<details>
<summary>♻️ Duplicate comments (1)</summary><blockquote>

<details>
<summary>src/error.ts (1)</summary><blockquote>

`28-42`: **Data handling error classes are well-designed.**

Both `KVError` and `StringToNumberConvertError` appropriately store the problematic values, which will help with debugging. I see the class name has been fixed from "CovertError" to "ConvertError" as suggested in previous reviews.

</blockquote></details>

</blockquote></details>

<details>
<summary>🧹 Nitpick comments (2)</summary><blockquote>

<details>
<summary>src/error.ts (2)</summary><blockquote>

`60-64`: **Simplify ExtractVideoMetadataError with inheritance.**

The constructor doesn't add any functionality beyond the parent constructor, making it unnecessary.

```diff
export class ExtractVideoMetadataError extends MyError {
-  constructor(message: string) {
-    super(message);
-  }
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 61-63: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

</details>

</details>

---

`74-90`: **Simplify these error classes by removing unnecessary constructors.**

Several classes have constructors that only call the parent constructor without additional logic, making them redundant.

```diff
export class MissingBinariesError extends MyError {
-  constructor(message: string) {
-    super(message);
-  }
}

export class NotSupportedError extends MyError {
-  constructor(message: string) {
-    super(message);
-  }
}

export class NothingToProcessError extends MyError {
-  constructor(message: string) {
-    super(message);
-  }
}
```

<details>
<summary>🧰 Tools</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

[error] 75-77: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 81-83: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 87-89: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

</details>

</details>

</blockquote></details>

</blockquote></details>

<details>
<summary>📜 Review details</summary>

**Configuration used: CodeRabbit UI**
**Review profile: CHILL**
**Plan: Pro**

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between bd1bfad26bb57d7fd3e79146bb1c5c1e95b07504 and 91721b0b2f8bdf9832a33362202cb0cfa7c2031b.

</details>

<details>
<summary>📒 Files selected for processing (5)</summary>

* `src/core/ffmpeg/command-builder.ts` (4 hunks)
* `src/core/video/metadata.ts` (3 hunks)
* `src/error.ts` (1 hunks)
* `src/features/encode-video/args.test.ts` (2 hunks)
* `src/features/encode-video/args.ts` (2 hunks)

</details>

<details>
<summary>🚧 Files skipped from review as they are similar to previous changes (4)</summary>

* src/features/encode-video/args.ts
* src/features/encode-video/args.test.ts
* src/core/ffmpeg/command-builder.ts
* src/core/video/metadata.ts

</details>

<details>
<summary>🧰 Additional context used</summary>

<details>
<summary>🪛 Biome (1.9.4)</summary>

<details>
<summary>src/error.ts</summary>

[error] 61-63: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 75-77: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 81-83: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

---

[error] 87-89: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.


(lint/complexity/noUselessConstructor)

</details>

</details>

</details>

<details>
<summary>🔇 Additional comments (6)</summary><blockquote>

<details>
<summary>src/error.ts (6)</summary>

`1-6`: **Good base error class implementation.**

This is a well-designed base class for your error hierarchy. Setting the name to the constructor name is a good practice for improved error identification during debugging.

---

`8-16`: **Comprehensive process error implementation.**

This `SpawnProcessError` implementation correctly captures both the underlying cause and stderr output, which will be valuable for debugging subprocess failures.

---

`18-26`: **Well-structured FFmpeg error hierarchy.**

Good separation of concerns by creating specific error types for FFmpeg-related operations. The empty `FFprobeProcessError` subclass properly differentiates FFprobe errors from general FFmpeg errors.

---

`44-58`: **Media processing error classes look good.**

The `FFprobeResultConvertError` and `EncodeImageError` classes properly propagate the original error as the cause, which will help maintain the error chain for debugging.

---

`66-72`: **Good implementation of RequiredFieldError.**

Storing the fields array will be helpful for providing detailed error information to users or logs.

---

`92-98`: **Good task error implementation.**

Storing the task name will help with debugging and potentially providing more meaningful error messages to users.

</details>

</blockquote></details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T08:08:53Z

Review comments:
diff_hunk: @@ -1,35 +1,48 @@
-import { log } from "@clack/prompts";
-import { checkFFmpegInstallation } from "../../core/ffmpeg/checker.js";
-import { getVideoInfoListFromUserInput } from "../../core/video/pipeline.js";
+import { spinner } from "@clack/prompts";
+import {
+  filterHev1Video,
+  getVideoInfoListFromUserInput,
+} from "../../core/video/pipeline.js";
+import { FFmpegProcessError, NothingToProcessError } from "../../error.js";
 import { runFFmpegCommand } from "../../libs/ffmpeg-executor.js";
 import type { EnableHEVCQuickTimeTaskProps, VideoInfo } from "../../types.js";
+import { getFileNameFromPath } from "../../utils/path.js";
 import { buildHEVCEnableQuickTimeArgs } from "./args.js";
 
 export async function enableHEVCQuickTimeTask({
   input,
 }: EnableHEVCQuickTimeTaskProps) {
-  checkFFmpegInstallation();
-
   const videoInfoList: VideoInfo[] = await getVideoInfoListFromUserInput(input);
-
-  const filterHev1Videos = videoInfoList.filter(
-    (v) =>
-      v.metadata.codec_name === "hevc" &&
-      v.metadata.codec_tag_string === "hev1",
-  );
-
-  for (let index = 0; index < filterHev1Videos.length; index++) {
-    const videoInfo = filterHev1Videos[index];
+  const filteredVideos = filterHev1Video(videoInfoList);
+  if (filteredVideos.length === 0) {
+    throw new NothingToProcessError("No hev1 videos found");
+  }
+  for (let index = 0; index < filteredVideos.length; index++) {
+    const videoInfo = filteredVideos[index];
     if (videoInfo !== undefined) {
-      log.message(
-        `processing ${index + 1}/${filterHev1Videos.length}:\n${videoInfo.input}`,
-      );
-      const result = await runFFmpegCommand(
-        buildHEVCEnableQuickTimeArgs(videoInfo),
-      );
-      if (result) {
-        log.success("complete!");
-      }
+      await enableHEVCQuickTime(videoInfo, index, filteredVideos.length);
+    }
+  }
+}
+
+async function enableHEVCQuickTime(
+  videoInfo: VideoInfo,
+  index: number,
+  length: number,
+) {
+  const s = spinner();
+  const filename = getFileNameFromPath(videoInfo.input);
+  s.start(`Remuxing: ${filename}`);
+  try {
+    await runFFmpegCommand(buildHEVCEnableQuickTimeArgs(videoInfo));
+    s.stop(`Finish remuxing ${index + 1}/${length}:\n${videoInfo.input}`);
+  } catch (error) {
+    let errMessage: string = `Failed to remux: ${videoInfo.input}\nerror: `;
+    if (error instanceof FFmpegProcessError) {
+      errMessage += error.cause.message;
+    } else {
+      errMessage += "unknown error";
     }
+    s.stop(errMessage);
   }
 }
path: src/features/enable-quickTime/index.ts
user: login: coderabbitai[bot]
body: _🛠️ Refactor suggestion_

**Good progress feedback implementation, but missing progress updates.**

The spinner provides good user feedback, but unlike the `encodeVideo` function, this doesn't use the progress handler parameter in `runFFmpegCommand`.


Consider adding progress updates for a consistent user experience:
```diff
-   await runFFmpegCommand(buildHEVCEnableQuickTimeArgs(videoInfo));
+   await runFFmpegCommand(
+     buildHEVCEnableQuickTimeArgs(videoInfo),
+     createProgressHandler(videoInfo.metadata.duration, {
+       message: (msg?: string) => {
+         s.message(`Remuxing ${filename}: ${msg}`);
+       },
+     }),
+   );
```

Also, enhance error handling for unknown errors:
```diff
    } else {
-     errMessage += "unknown error";
+     errMessage += error instanceof Error ? error.message : String(error);
    }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
async function enableHEVCQuickTime(
  videoInfo: VideoInfo,
  index: number,
  length: number,
) {
  const s = spinner();
  const filename = getFileNameFromPath(videoInfo.input);
  s.start(`Remuxing: ${filename}`);
  try {
    await runFFmpegCommand(
      buildHEVCEnableQuickTimeArgs(videoInfo),
      createProgressHandler(videoInfo.metadata.duration, {
        message: (msg?: string) => {
          s.message(`Remuxing ${filename}: ${msg}`);
        },
      }),
    );
    s.stop(`Finish remuxing ${index + 1}/${length}:\n${videoInfo.input}`);
  } catch (error) {
    let errMessage: string = `Failed to remux: ${videoInfo.input}\nerror: `;
    if (error instanceof FFmpegProcessError) {
      errMessage += error.cause.message;
    } else {
      errMessage += error instanceof Error ? error.message : String(error);
    }
    s.stop(errMessage);
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-29T07:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,24 +38,20 @@ export async function getMetadataToVideoList(videoPaths: string[]) {
     const tasks = videoPaths.map((path) =>
       limit(() => processSingleVideo(path)),
     );
-
     const results = await Promise.allSettled(tasks);
-
     const successfulResults: VideoInfo[] = [];
-    const errorLogger = console.error.bind(console);
-
+    // const errorLogger = console.error.bind(console);
     results.forEach((result) => {
       if (result.status === "fulfilled") {
         successfulResults.push(result.value);
       } else {
-        errorLogger(`Processing failed: ${result.reason.message}`);
         // TODO: report error
+        // errorLogger(`Processing failed: ${result.reason.message}`);
       }
     });
-
     return successfulResults;
-  } catch (error) {
-    console.error("Unexpected pipeline error:", error);
+  } catch {
+    // console.error("Unexpected pipeline error:", error);
     return [];
   }
path: src/core/video/pipeline.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Empty catch block swallows all errors.**

The catch block catches all errors without type information and silently returns an empty array, which could hide important issues.


```diff
- } catch {
+ } catch (error) {
+   console.warn("Pipeline error:", error instanceof Error ? error.message : String(error));
    return [];
  }
```


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
  } catch (error) {
    // console.error("Unexpected pipeline error:", error);
    console.warn("Pipeline error:", error instanceof Error ? error.message : String(error));
    return [];
  }
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-29T07:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -36,24 +38,20 @@ export async function getMetadataToVideoList(videoPaths: string[]) {
     const tasks = videoPaths.map((path) =>
       limit(() => processSingleVideo(path)),
     );
-
     const results = await Promise.allSettled(tasks);
-
     const successfulResults: VideoInfo[] = [];
-    const errorLogger = console.error.bind(console);
-
+    // const errorLogger = console.error.bind(console);
     results.forEach((result) => {
       if (result.status === "fulfilled") {
         successfulResults.push(result.value);
       } else {
-        errorLogger(`Processing failed: ${result.reason.message}`);
         // TODO: report error
+        // errorLogger(`Processing failed: ${result.reason.message}`);
       }
path: src/core/video/pipeline.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Commented-out error logging without replacement.**

The error logging has been commented out, but there's no clear replacement for reporting these errors.


Consider implementing a replacement approach for error reporting, such as:
- Collecting errors for later reporting
- Implementing a debug flag-controlled logging mechanism
- Using a structured logging system

---

🏁 Script executed:

```shell
#!/bin/bash
# Find potential error handling patterns elsewhere in the codebase
rg -A 2 "catch \(.*\) \{" --type ts | grep -v "\/\/"
```

Length of output: 1823

---

🏁 Script executed:

```shell
#!/bin/bash
# Display the first 200 lines of the pipeline to understand error handling context
sed -n '1,200p' src/core/video/pipeline.ts
```

Length of output: 2757

---



</details>

**Implement structured error reporting in `getMetadataToVideoList`**

Currently any per-video errors are swallowed (the console calls are commented out), which makes debugging failures downstream difficult. Let’s replace the commented logger with a simple, configurable error-collection or logging mechanism:

– Add an array to collect or log errors alongside successes.  
– Optionally gate console output behind a debug flag or use a structured logger.  
– Return or expose both successful results and errors (or rethrow an aggregated error) so callers can react accordingly.

For example:

```diff
--- a/src/core/video/pipeline.ts
+++ b/src/core/video/pipeline.ts
@@ async function getMetadataToVideoList(videoPaths: string[]) {
-   const successfulResults: VideoInfo[] = [];
-   // const errorLogger = console.error.bind(console);
+   const successfulResults: VideoInfo[] = [];
+   const errorMessages: string[] = [];
+   const debug = process.env.DEBUG_PIPELINE === "true";
@@ results.forEach((result) => {
-    if (result.status === "fulfilled") {
+    if (result.status === "fulfilled") {
       successfulResults.push(result.value);
     } else {
-      // TODO: report error
-      // errorLogger(`Processing failed: ${result.reason.message}`);
+      const msg = `Processing failed: ${result.reason.message}`;
+      errorMessages.push(msg);
+      if (debug) console.error(msg);
     }
   });
+  if (errorMessages.length) {
+    // e.g. throw new AggregateError(errorMessages, "Some videos failed");
+    /* or return both successes & errors:
+       return { successfulResults, errorMessages };
+    */
+  }
   return successfulResults;
```

Locations to update:
- `src/core/video/pipeline.ts` lines 43–50  
- Add any config or environment‐variable handling for `DEBUG_PIPELINE`  

This ensures errors aren’t lost and gives callers full visibility into which files failed and why.  


<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
    const successfulResults: VideoInfo[] = [];
    const errorMessages: string[] = [];
    const debug = process.env.DEBUG_PIPELINE === "true";

    results.forEach((result) => {
      if (result.status === "fulfilled") {
        successfulResults.push(result.value);
      } else {
        const msg = `Processing failed: ${result.reason.message}`;
        errorMessages.push(msg);
        if (debug) console.error(msg);
      }
    });

    if (errorMessages.length) {
      // e.g. throw new AggregateError(errorMessages, "Some videos failed");
      /* or return both successes & errors:
         return { successfulResults, errorMessages };
      */
    }

    return successfulResults;
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-29T07:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,98 @@
+class MyError extends Error {
+  constructor(message: string) {
+    super(message);
+    this.name = this.constructor.name;
+  }
+}
+
+export class SpawnProcessError extends MyError {
+  public override cause: Error;
+  public stderr: string;
+  constructor(message: string, stderr: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+    this.stderr = stderr;
+  }
+}
+
+export class FFmpegProcessError extends MyError {
+  public override cause: Error;
+  constructor(message: string, cause: Error) {
+    super(message);
+    this.cause = cause;
+  }
+}
+
+export class FFprobeProcessError extends FFmpegProcessError {}
+
+export class KVError extends MyError {
+  public pair: string;
+  constructor(message: string, pair: string) {
+    super(message);
+    this.pair = pair;
+  }
+}
+
+export class StringToNumberCovertError extends MyError {
+  public value: string;
+  constructor(message: string, value: string) {
+    super(message);
+    this.value = value;
+  }
+}
path: src/error.ts
user: login: coderabbitai[bot]
body: _💡 Verification agent_

<details>
<summary>🧩 Analysis chain</summary>

**Typo in class name – should be “Convert”, not “Covert”**

`StringToNumberCovertError` contains a misspelling that will quickly propagate throughout the codebase and docs.  
Rename the class to maintain clarity and discoverability.

```diff
-export class StringToNumberCovertError extends MyError {
+export class StringToNumberConvertError extends MyError {
```


Please grep the codebase for all current usages of `StringToNumberCovertError` and update the references as well.

---

🏁 Script executed:

```shell
#!/bin/bash
# Search for all occurrences of the misspelled class name in the codebase
rg "StringToNumberCovertError" -n
```

Length of output: 268

---



</details>

**Rename typo in error class “StringToNumberCovertError” to “StringToNumberConvertError”**

Please update the class name and all its references:

• src/error.ts (line 36):  
```diff
-export class StringToNumberCovertError extends MyError {
+export class StringToNumberConvertError extends MyError {
```

• src/core/video/metadata.ts (line 5 import):  
```diff
-import { StringToNumberCovertError } from '../../error';
+import { StringToNumberConvertError } from '../../error';
```

• src/core/video/metadata.ts (line 54 throw):  
```diff
-    throw new StringToNumberCovertError(`Invalid ${key} value`, value);
+    throw new StringToNumberConvertError(`Invalid ${key} value`, value);
```



<!-- suggestion_start -->

<details>
<summary>📝 Committable suggestion</summary>

> ‼️ **IMPORTANT**
> Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

`````suggestion
export class StringToNumberConvertError extends MyError {
  public value: string;
  constructor(message: string, value: string) {
    super(message);
    this.value = value;
  }
}
`````

</details>

<!-- suggestion_end -->

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-29T07:32:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -43,11 +34,16 @@ export class FFmpegCommandBuilder {
     return this;
   }
 
-  setPreset(preset: string = "veryfast"): this {
+  setPreset(preset: string = "medium"): this {
     this.outputOptions.push("-preset", preset);
     return this;
   }
 
+  setPixFmt(fmt: string = "yuv420p"): this {
+    this.outputOptions.push("pix_fmt", fmt);
+    return this;
+  }
path: src/core/ffmpeg/command-builder.ts
user: login: coderabbitai[bot]
body: _⚠️ Potential issue_

**Missing dash prefix breaks FFmpeg CLI – use `-pix_fmt`**

FFmpeg expects the option name to start with a dash.  
Without it, `pix_fmt` will be interpreted as a positional argument and the command will fail.

```diff
-  this.outputOptions.push("pix_fmt", fmt);
+  this.outputOptions.push("-pix_fmt", fmt);
```

<!-- This is an auto-generated comment by CodeRabbit -->
created_at: 2025-04-29T07:32:45Z
author_association: CONTRIBUTOR

PR comments:
user: login: coderabbitai[bot]
created_at: 2025-04-29T07:29:07Z
body: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- walkthrough_start -->

## Walkthrough

This update introduces a comprehensive refactor and enhancement of the codebase, focusing on error handling, video encoding configuration, and test utilities. A new centralized error module defines custom error classes, replacing generic errors throughout the code. The FFmpeg command builder is extended with an H.265-specific subclass to support advanced encoding options, including bit depth and pixel format handling. The video and image processing pipelines are updated to use these new error types and builder features. Task handling in the CLI is modularized, and progress reporting is improved. Extensive test updates and new utilities facilitate robust testing and validation.

## Changes

| File(s) | Change Summary |
|---------|---------------|
| `.changeset/cool-eels-fry.md` | Added a changeset documenting a minor update for bit depth encoding support. |
| `.github/workflows/sync-metadata.yml` | Deleted GitHub Actions workflow for syncing repository metadata from `package.json`. |
| `fixtures/ffprobe output.txt` | Inserted blank lines for improved separation in ffprobe output. |
| `src/core/ffmpeg/checker.ts` | Changed missing binary error from generic `Error` to `MissingBinariesError`. |
| `src/core/ffmpeg/command-builder.ts` | Changed field visibility to protected, simplified encoder method, changed default preset, added `setPixFmt`, and introduced `FFmpegH265CommandBuilder` with H.265/x265-specific methods. |
| `src/core/image/encode.ts` | Refactored to use async/await, improved error handling with custom error classes, removed console logging. |
| `src/core/image/pipeline.ts` | Replaced generic error with `NothingToProcessError` when no images are found. |
| `src/core/video/metadata.test.ts` | Updated tests to use utility functions, improved error expectation, added edge case coverage, removed legacy tests. |
| `src/core/video/metadata.ts` | Introduced custom error classes, adjusted required fields, improved parsing and error specificity. |
| `src/core/video/pipeline.test.ts` | Updated to use test utilities, expect custom errors, removed console error spies, added tests for `filterHev1Video`. |
| `src/core/video/pipeline.ts` | Replaced generic errors with custom error classes, improved error handling, added `filterHev1Video` export. |
| `src/error.ts` | New module defining a hierarchy of custom error classes for process, conversion, and domain errors. |
| `src/features/cli/index.test.ts` | Updated mocks and assertions for renamed task functions, added ffmpeg check tests, improved error path coverage. |
| `src/features/cli/index.ts` | Refactored task dispatch to use a handler map, centralized ffmpeg installation check. |
| `src/features/cli/progress-handler.test.ts` | New test suite for progress handler covering completion, throttling, formatting, and edge cases. |
| `src/features/cli/progress-handler.ts` | Refactored to accept a message handler, improved progress checks, formatted output, removed commented logs. |
| `src/features/enable-quickTime/args.test.ts` | Switched to utility-based test data, granular assertions on command output. |
| `src/features/enable-quickTime/index.ts` | Refactored to use `filterHev1Video`, modularized processing with spinner feedback, improved error handling. |
| `src/features/encode-image/index.ts` | Renamed export, changed error handling to use `EncodeImageError`, simplified result processing. |
| `src/features/encode-video/args.test.ts` | Refactored to use utility-based test data, added tests for pixel format affecting profile. |
| `src/features/encode-video/args.ts` | Switched to `FFmpegH265CommandBuilder`, added support for pixel format and bit depth, removed preset setting. |
| `src/features/encode-video/index.ts` | Renamed export, delegated encoding to helper function, added spinner-based feedback and improved error reporting. |
| `src/index.ts` | Improved error logging, added explicit handling for `NothingToProcessError`. |
| `src/libs/ffmpeg-executor.ts` | Centralized error handling with custom error classes, included process signals in errors, removed direct logging. |
| `src/types.ts` | Swapped and updated `VideoInfo` and `ImageInfo` interfaces, updated metadata fields for video. |
| `src/utils/basic-convert.ts` | Added `tryConvertStringToNumber` utility for safe string-to-number conversion. |
| `src/utils/test-utils.ts` | New test utility functions for creating mock video info and metadata objects. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant CLI
    participant TaskHandler
    participant FFmpegChecker
    participant Encoder
    participant ErrorModule

    CLI->>TaskHandler: runCli(task, args)
    TaskHandler->>FFmpegChecker: checkFFmpegInstallation() (if required)
    alt FFmpeg missing
        FFmpegChecker-->>TaskHandler: throw MissingBinariesError
        TaskHandler-->>CLI: propagate error
    else FFmpeg present
        TaskHandler->>Encoder: encodeVideoTask/encodeImageTask/enableHEVCQuickTimeTask
        Encoder->>ErrorModule: throw custom errors on failure
        Encoder-->>TaskHandler: result or error
        TaskHandler-->>CLI: result or error
    end
```

```mermaid
sequenceDiagram
    participant Encoder
    participant FFmpegH265CommandBuilder
    participant FFmpegExecutor
    participant ErrorModule

    Encoder->>FFmpegH265CommandBuilder: configure (set profile, bit depth, etc.)
    FFmpegH265CommandBuilder-->>Encoder: build command args
    Encoder->>FFmpegExecutor: runFFmpegCommand(args, progressHandler)
    FFmpegExecutor->>ErrorModule: throw FFmpegProcessError/FFprobeProcessError on failure
    FFmpegExecutor-->>Encoder: result or error
    Encoder-->>Caller: result or error
```

## Possibly related PRs

- [codenoobforreal/clitools#23](https://github.com/codenoobforreal/clitools/pull/23): Introduced the initial `FFmpegCommandBuilder` and `FFprobeCommandBuilder` classes, which are extended and refactored in this PR for advanced encoding and error handling.

## Poem

> 🐇  
> A hop, a skip, a bit-depth leap,  
> With errors tamed and types that keep,  
> The builder learns new tricks today,  
> For H.265, we pave the way!  
> Progress spins, the spinners twirl,  
> Test utilities in a whirl—  
> This bunny’s code now shines so bright,  
> Encoding dreams in bits of light!  
>

<!-- walkthrough_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---



<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=codenoobforreal/clitools&utm_content=28):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
author_association: CONTRIBUTOR
user: login: codenoobforreal
created_at: 2025-04-29T08:07:49Z
body: @coderabbitai plz review my new commit 91721b0
author_association: OWNER
user: login: coderabbitai[bot]
created_at: 2025-04-29T08:09:38Z
body: <!-- This is an auto-generated reply by CodeRabbit -->
> [!TIP]
> For best results, initiate chat on the files or code changes.

<details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Get the specific commit details
git show --name-status 91721b0

# Show the actual code changes
git show 91721b0
```

Length of output: 124692

---

🏁 Script executed:
```
#!/bin/bash
# Let's focus on the specific changes related to pix_fmt in this commit
git show 91721b0 -- "src/features/encode-video/args.ts"

# Also check for changes in FFmpeg command builder which might be related
git show 91721b0 -- "src/core/ffmpeg/command-builder.ts"

# Let's also look at tests that might confirm the fix
git show 91721b0 -- "src/features/encode-video/args.test.ts"

# Check if there are other files that reference pix_fmt
git grep "pix_fmt" 91721b0
```

Length of output: 10345

---



</details>

@codenoobforreal I've reviewed commit 91721b0 "fix: prefix pix_fmt with dash". This is a solid fix that ensures pixel format parameters are properly passed to FFmpeg with the correct command-line syntax.

The fix correctly implements the `setPixFmt` method in `FFmpegCommandBuilder` class to push two separate items to the command array:
```typescript
this.outputOptions.push("-pix_fmt", fmt);
```

This ensures FFmpeg receives the command in the proper syntax (`-pix_fmt yuv420p`) rather than an incorrect format like `pix_fmt=yuv420p`. The latter format appears in ffprobe output but isn't valid for FFmpeg command-line arguments.

Your tests confirm this behavior works correctly for various pixel formats including `yuv420p`, `yuv420p10le`, `yuv422p10le`, and `yuv444p10le`. This fix will prevent command execution errors when encoding videos with different pixel formats.

Nice work on catching and fixing this subtle but important syntax issue!


<!-- <agentComponentType>chat-answer</agentComponentType> -->

<!-- <rawResChunk><answer><details>
<summary>🧩 Analysis chain</summary>

🏁 Script executed:
```
#!/bin/bash
# Get the specific commit details
git show --name-status 91721b0

# Show the actual code changes
git show 91721b0
```

Length of output: 124692

---

🏁 Script executed:
```
#!/bin/bash
# Let's focus on the specific changes related to pix_fmt in this commit
git show 91721b0 -- "src/features/encode-video/args.ts"

# Also check for changes in FFmpeg command builder which might be related
git show 91721b0 -- "src/core/ffmpeg/command-builder.ts"

# Let's also look at tests that might confirm the fix
git show 91721b0 -- "src/features/encode-video/args.test.ts"

# Check if there are other files that reference pix_fmt
git grep "pix_fmt" 91721b0
```

Length of output: 10345

---



</details>

@codenoobforreal I've reviewed commit 91721b0 "fix: prefix pix_fmt with dash". This is a solid fix that ensures pixel format parameters are properly passed to FFmpeg with the correct command-line syntax.

The fix correctly implements the `setPixFmt` method in `FFmpegCommandBuilder` class to push two separate items to the command array:
```typescript
this.outputOptions.push("-pix_fmt", fmt);
```

This ensures FFmpeg receives the command in the proper syntax (`-pix_fmt yuv420p`) rather than an incorrect format like `pix_fmt=yuv420p`. The latter format appears in ffprobe output but isn't valid for FFmpeg command-line arguments.

Your tests confirm this behavior works correctly for various pixel formats including `yuv420p`, `yuv420p10le`, `yuv422p10le`, and `yuv444p10le`. This fix will prevent command execution errors when encoding videos with different pixel formats.

Nice work on catching and fixing this subtle but important syntax issue!</answer></rawResChunk> -->
author_association: CONTRIBUTOR