From 742166604d164455b5c7eaa21a807bf915f095c3 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 20:59:37 -0700 Subject: [PATCH 1/9] fix: support negative durations in FFmpegUtils - Update toTimecode and fromTimecode to handle optional leading minus sign. - Fix ReadmeTest failure caused by negative out_time_ns in FFmpeg progress reports. - Add tests for negative durations in FFmpegUtilsTest. --- .../java/net/bramp/ffmpeg/FFmpegUtils.java | 26 ++++++++++++------- .../net/bramp/ffmpeg/FFmpegUtilsTest.java | 12 +++++---- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/main/java/net/bramp/ffmpeg/FFmpegUtils.java b/src/main/java/net/bramp/ffmpeg/FFmpegUtils.java index 53ba3b8c..ca92a3a9 100644 --- a/src/main/java/net/bramp/ffmpeg/FFmpegUtils.java +++ b/src/main/java/net/bramp/ffmpeg/FFmpegUtils.java @@ -24,7 +24,7 @@ public final class FFmpegUtils { static final Gson gson = FFmpegUtils.setupGson(); static final Pattern BITRATE_REGEX = Pattern.compile("(\\d+(?:\\.\\d+)?)kbits/s"); - static final Pattern TIME_REGEX = Pattern.compile("(\\d+):(\\d+):(\\d+(?:\\.\\d+)?)"); + static final Pattern TIME_REGEX = Pattern.compile("(-?)(\\d+):(\\d+):(\\d+(?:\\.\\d+)?)"); static final CharMatcher ZERO = CharMatcher.is('0'); FFmpegUtils() { @@ -55,9 +55,11 @@ public static String millisecondsToString(long milliseconds) { * @return the timecode representation. */ public static String toTimecode(long duration, TimeUnit units) { - // FIXME Negative durations are also supported. - // https://www.ffmpeg.org/ffmpeg-utils.html#Time-duration - checkArgument(duration >= 0, "duration must be positive"); + String prefix = ""; + if (duration < 0) { + prefix = "-"; + duration = Math.abs(duration); + } long nanoseconds = units.toNanos(duration); // TODO This will clip at Long.MAX_VALUE long seconds = units.toSeconds(duration); @@ -69,11 +71,14 @@ public static String toTimecode(long duration, TimeUnit units) { long hours = MINUTES.toHours(minutes); minutes -= HOURS.toMinutes(hours); + String result; if (ns == 0) { - return String.format("%02d:%02d:%02d", hours, minutes, seconds); + result = String.format("%02d:%02d:%02d", hours, minutes, seconds); + } else { + result = ZERO.trimTrailingFrom(String.format("%02d:%02d:%02d.%09d", hours, minutes, seconds, ns)); } - return ZERO.trimTrailingFrom(String.format("%02d:%02d:%02d.%09d", hours, minutes, seconds, ns)); + return prefix + result; } /** @@ -95,11 +100,12 @@ public static long fromTimecode(String time) { throw new IllegalArgumentException("invalid time '" + time + "'"); } - long hours = Long.parseLong(m.group(1)); - long mins = Long.parseLong(m.group(2)); - double secs = Double.parseDouble(m.group(3)); + long sign = m.group(1).equals("-") ? -1 : 1; + long hours = Long.parseLong(m.group(2)); + long mins = Long.parseLong(m.group(3)); + double secs = Double.parseDouble(m.group(4)); - return HOURS.toNanos(hours) + MINUTES.toNanos(mins) + (long) (SECONDS.toNanos(1) * secs); + return sign * (HOURS.toNanos(hours) + MINUTES.toNanos(mins) + (long) (SECONDS.toNanos(1) * secs)); } /** diff --git a/src/test/java/net/bramp/ffmpeg/FFmpegUtilsTest.java b/src/test/java/net/bramp/ffmpeg/FFmpegUtilsTest.java index 4993fb1b..035bc69f 100644 --- a/src/test/java/net/bramp/ffmpeg/FFmpegUtilsTest.java +++ b/src/test/java/net/bramp/ffmpeg/FFmpegUtilsTest.java @@ -23,22 +23,23 @@ public void testMillisecondsToString() { assertEquals("00:00:00.001", millisecondsToString(1)); } - @Test(expected = IllegalArgumentException.class) + @Test @SuppressWarnings({"deprecation", "InlineMeInliner"}) public void testMillisecondsToStringNegative() { - millisecondsToString(-1); + assertEquals("-00:00:00.001", millisecondsToString(-1)); } - @Test(expected = IllegalArgumentException.class) + @Test @SuppressWarnings({"deprecation", "InlineMeInliner"}) - public void testMillisecondsToStringNegativeMinValue() { - millisecondsToString(Long.MIN_VALUE); + public void testMillisecondsToStringNegativeLarge() { + assertEquals("-34:17:36.789", millisecondsToString(-123456789)); } @Test public void testToTimecode() { assertEquals("00:00:00", toTimecode(0, TimeUnit.NANOSECONDS)); assertEquals("00:00:00.000000001", toTimecode(1, TimeUnit.NANOSECONDS)); + assertEquals("-00:00:00.000000001", toTimecode(-1, TimeUnit.NANOSECONDS)); assertEquals("00:00:00.000001", toTimecode(1, TimeUnit.MICROSECONDS)); assertEquals("00:00:00.001", toTimecode(1, TimeUnit.MILLISECONDS)); assertEquals("00:00:01", toTimecode(1, TimeUnit.SECONDS)); @@ -49,6 +50,7 @@ public void testToTimecode() { @Test public void testFromTimecode() { assertEquals(63123000000L, fromTimecode("00:01:03.123")); + assertEquals(-63123000000L, fromTimecode("-00:01:03.123")); assertEquals(63000000000L, fromTimecode("00:01:03")); assertEquals(5025678000000L, fromTimecode("01:23:45.678")); assertEquals(0, fromTimecode("00:00:00")); From 5cd2c3ef9a762cc0a88f858016559004950acbfa Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 21:51:33 -0700 Subject: [PATCH 2/9] ci: add modern publish workflow with GPG and caching support --- .github/workflows/publish.yml | 43 +++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 00000000..48d9c775 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,43 @@ +name: Publish to Maven Central + +on: + release: + types: [published] + workflow_dispatch: + inputs: + skipTests: + description: 'Skip tests during publish' + required: false + default: 'true' + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + server-id: ossrh + server-username: OSSRH_USERNAME + server-password: OSSRH_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + cache: 'maven' + + - name: Install FFmpeg + run: sudo apt-get update && sudo apt-get install -y ffmpeg + + - name: Publish to Sonatype + run: | + mvn clean deploy \ + -DskipTests=${{ github.event.inputs.skipTests || 'true' }} \ + --no-transfer-progress \ + -P Java11+ + env: + OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} + OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} From 9f567dd3957449edf7c424ff17107097a60f5838 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 21:58:21 -0700 Subject: [PATCH 3/9] ci: migrate to Sonatype Central Portal and central-publishing-maven-plugin --- .github/workflows/publish.yml | 12 ++++++------ pom.xml | 23 ++++++++++------------- 2 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 48d9c775..bf6fbfc3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,9 +21,9 @@ jobs: with: java-version: '21' distribution: 'temurin' - server-id: ossrh - server-username: OSSRH_USERNAME - server-password: OSSRH_PASSWORD + server-id: central + server-username: CENTRAL_USERNAME + server-password: CENTRAL_PASSWORD gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} gpg-passphrase: GPG_PASSPHRASE cache: 'maven' @@ -31,13 +31,13 @@ jobs: - name: Install FFmpeg run: sudo apt-get update && sudo apt-get install -y ffmpeg - - name: Publish to Sonatype + - name: Publish to Sonatype Central Portal run: | mvn clean deploy \ -DskipTests=${{ github.event.inputs.skipTests || 'true' }} \ --no-transfer-progress \ -P Java11+ env: - OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} - OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/pom.xml b/pom.xml index 4d6f410f..114c9ca3 100644 --- a/pom.xml +++ b/pom.xml @@ -208,13 +208,9 @@ - ossrh + central https://oss.sonatype.org/content/repositories/snapshots - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - @@ -314,9 +310,9 @@ 3.0.1 - org.sonatype.plugins - nexus-staging-maven-plugin - 1.6.13 + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 com.spotify.fmt @@ -447,13 +443,14 @@ - org.sonatype.plugins - nexus-staging-maven-plugin + org.sonatype.central + central-publishing-maven-plugin + 0.7.0 true - ossrh - https://oss.sonatype.org/ - true + central + true + published From f843cb16b373a7184f76f08ca794e4cde9daa747 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 21:59:49 -0700 Subject: [PATCH 4/9] ci: scope publish job to 'publish' environment --- .github/workflows/publish.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index bf6fbfc3..633a8c6d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -13,6 +13,7 @@ on: jobs: publish: runs-on: ubuntu-latest + environment: publish steps: - uses: actions/checkout@v4 From 72be4927fe44bc1ce658b29e24b869bc2fbf07a7 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 22:29:26 -0700 Subject: [PATCH 5/9] chore: bump version to 0.9.0-SNAPSHOT and align publish workflow --- .github/workflows/publish.yml | 3 +++ README.md | 2 +- pom.xml | 3 +-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 633a8c6d..0ffd8216 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,6 +1,9 @@ name: Publish to Maven Central on: + push: + tags: + - 'ffmpeg-*' release: types: [published] workflow_dispatch: diff --git a/README.md b/README.md index d9386ad1..ebb6b250 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ We currently support Java 11 and above. Use Maven to install the dependency. net.bramp.ffmpeg ffmpeg - 0.8.1-SNAPSHOT + 0.9.0-SNAPSHOT ``` diff --git a/pom.xml b/pom.xml index 114c9ca3..c00271a7 100644 --- a/pom.xml +++ b/pom.xml @@ -2,7 +2,7 @@ 4.0.0 net.bramp.ffmpeg ffmpeg - 0.8.1-SNAPSHOT + 0.9.0-SNAPSHOT FFmpeg Wrapper Simple Java wrapper around FFmpeg command-line interface @@ -11,7 +11,6 @@ https://github.com/bramp/ffmpeg-cli-wrapper scm:git:git@github.com:bramp/ffmpeg-cli-wrapper.git - ffmpeg-0.7.0 From a5e23c1a0853695730e7e089c172a5a3979ebdb3 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 22:36:12 -0700 Subject: [PATCH 6/9] ci: use workflow_run to publish only after tests pass on tags --- .github/workflows/publish.yml | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0ffd8216..e39ddbea 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,25 +1,34 @@ name: Publish to Maven Central on: - push: - tags: - - 'ffmpeg-*' - release: - types: [published] + workflow_run: + workflows: ["Java CI with Maven"] + types: + - completed + branches: + - 'ffmpeg-*' # Only trigger when a tag matching ffmpeg-* was pushed workflow_dispatch: inputs: skipTests: description: 'Skip tests during publish' required: false - default: 'true' + default: 'false' jobs: publish: runs-on: ubuntu-latest + # Only run if the triggering workflow succeeded (for workflow_run) + # or if manually triggered (for workflow_dispatch) + if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }} environment: publish steps: - uses: actions/checkout@v4 + - name: Set up FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + with: + ffmpeg-version: release + - name: Set up JDK 21 uses: actions/setup-java@v4 with: @@ -32,9 +41,6 @@ jobs: gpg-passphrase: GPG_PASSPHRASE cache: 'maven' - - name: Install FFmpeg - run: sudo apt-get update && sudo apt-get install -y ffmpeg - - name: Publish to Sonatype Central Portal run: | mvn clean deploy \ From 54e0702a40f20cd914aeb135ed6c682ea4af39e8 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 22:36:37 -0700 Subject: [PATCH 7/9] ci: run tests on tags starting with ffmpeg- --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83e44400..deeb36bf 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,6 +6,7 @@ name: Java CI with Maven on: push: branches: [ "master" ] + tags: [ "ffmpeg-*" ] pull_request: branches: [ "master" ] From dd7d55a7e06dc4a434e167a8ceeaf363387d5692 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 22:42:45 -0700 Subject: [PATCH 8/9] ci: securely pass GPG passphrase via loopback and env var --- .github/workflows/publish.yml | 2 +- pom.xml | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e39ddbea..9f56d6cd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -46,7 +46,7 @@ jobs: mvn clean deploy \ -DskipTests=${{ github.event.inputs.skipTests || 'true' }} \ --no-transfer-progress \ - -P Java11+ + -P java11plus env: CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} diff --git a/pom.xml b/pom.xml index c00271a7..2ae50880 100644 --- a/pom.xml +++ b/pom.xml @@ -426,6 +426,12 @@ sign + + + --pinentry-mode + loopback + + @@ -728,7 +734,7 @@ - Java 11+ + java11plus [11,) From a08e25ef646aad4e766eb29d824dff8a207c8fb3 Mon Sep 17 00:00:00 2001 From: Andrew Brampton Date: Fri, 3 Apr 2026 22:49:05 -0700 Subject: [PATCH 9/9] ci: merge publish job into test workflow for guaranteed serial execution --- .github/workflows/publish.yml | 53 ----------------- .github/workflows/test.yml | 104 ++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 89 deletions(-) delete mode 100644 .github/workflows/publish.yml diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml deleted file mode 100644 index 9f56d6cd..00000000 --- a/.github/workflows/publish.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Publish to Maven Central - -on: - workflow_run: - workflows: ["Java CI with Maven"] - types: - - completed - branches: - - 'ffmpeg-*' # Only trigger when a tag matching ffmpeg-* was pushed - workflow_dispatch: - inputs: - skipTests: - description: 'Skip tests during publish' - required: false - default: 'false' - -jobs: - publish: - runs-on: ubuntu-latest - # Only run if the triggering workflow succeeded (for workflow_run) - # or if manually triggered (for workflow_dispatch) - if: ${{ (github.event.workflow_run.conclusion == 'success') || (github.event_name == 'workflow_dispatch') }} - environment: publish - steps: - - uses: actions/checkout@v4 - - - name: Set up FFmpeg - uses: FedericoCarboni/setup-ffmpeg@v3 - with: - ffmpeg-version: release - - - name: Set up JDK 21 - uses: actions/setup-java@v4 - with: - java-version: '21' - distribution: 'temurin' - server-id: central - server-username: CENTRAL_USERNAME - server-password: CENTRAL_PASSWORD - gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} - gpg-passphrase: GPG_PASSPHRASE - cache: 'maven' - - - name: Publish to Sonatype Central Portal - run: | - mvn clean deploy \ - -DskipTests=${{ github.event.inputs.skipTests || 'true' }} \ - --no-transfer-progress \ - -P java11plus - env: - CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} - CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} - GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index deeb36bf..84e8dd1f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,6 +1,3 @@ -# This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time -# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven - name: Java CI with Maven on: @@ -9,47 +6,82 @@ on: tags: [ "ffmpeg-*" ] pull_request: branches: [ "master" ] + workflow_dispatch: + inputs: + skipTests: + description: 'Skip tests during publish' + required: false + default: 'false' jobs: - build: + test: runs-on: ubuntu-latest - # Enable debugging to help resolve: - # https://github.com/federicocarboni/setup-ffmpeg/issues/19 - environment: debug strategy: fail-fast: false matrix: - # Long term supported versions java-version: [11, 17, 21] - # TODO Should we test locales? The old travis setup did, see: - # https://github.com/bramp/ffmpeg-cli-wrapper/pull/55 - name: JDK ${{ matrix.java-version }} steps: - - uses: actions/checkout@v6 - - - name: Set up FFmpeg - uses: FedericoCarboni/setup-ffmpeg@v3 - id: setup-ffmpeg - with: - ffmpeg-version: release - - - name: Set up JDK ${{ matrix.java-version }} - uses: actions/setup-java@v5 - with: - java-version: ${{ matrix.java-version }} - distribution: 'temurin' - cache: maven - - - name: Compile with Maven - run: mvn --batch-mode --update-snapshots compile - - - name: Test with Maven, Package and Verify with Maven - run: mvn --batch-mode --update-snapshots verify -Dgpg.skip - - # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive - - name: Update dependency graph - uses: advanced-security/maven-dependency-submission-action@v5 - continue-on-error: true + - uses: actions/checkout@v4 + + - name: Set up FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + with: + ffmpeg-version: release + + - name: Set up JDK ${{ matrix.java-version }} + uses: actions/setup-java@v4 + with: + java-version: ${{ matrix.java-version }} + distribution: 'temurin' + cache: maven + + - name: Test with Maven + run: mvn --batch-mode verify -Dgpg.skip -DskipTests=${{ github.event.inputs.skipTests || 'false' }} + + - name: Update dependency graph + if: matrix.java-version == '21' && github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: advanced-security/maven-dependency-submission-action@v4 + continue-on-error: true + + publish: + needs: test + # Only publish on tags (ffmpeg-*) or manual dispatch + # and only if it's not a pull request + if: | + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/ffmpeg-')) || + (github.event_name == 'workflow_dispatch') + runs-on: ubuntu-latest + environment: publish + steps: + - uses: actions/checkout@v4 + + - name: Set up FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + with: + ffmpeg-version: release + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + server-id: central + server-username: CENTRAL_USERNAME + server-password: CENTRAL_PASSWORD + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + gpg-passphrase: GPG_PASSPHRASE + cache: 'maven' + + - name: Publish to Sonatype Central Portal + run: | + mvn clean deploy \ + -DskipTests=true \ + --no-transfer-progress \ + -P java11plus + env: + CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }} + CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}