定制 iceylan/subtitle 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

iceylan/subtitle

Composer 安装命令:

composer require iceylan/subtitle

包简介

A comprehensive subtitle parser and renderer library.

README 文档

README

Latest Version on Packagist Total Downloads Software License

Iceylan\Subtitle is a modern, enterprise-grade, highly extensible, and 100% Immutable subtitle manipulation library for PHP. Built on robust software design patterns, it allows you to parse, convert, shift time, change frame rates, and programmatically resolve subtitle overlaps without any unexpected side effects.

Features

  • Fluent API: Clean and readable method chaining.
  • Smart Formats: Auto-detects subtitle formats.
  • Immutability: Keeps your original subtitle data safe from side effects.
  • Overlap Resolution: Built-in strategies to clean overlapping subtitle timestamps.
  • Format Conversion: Convert between different subtitle formats.
  • Time Shifting: Change FPS, stretch by anchors or just move subtitles forward or backward in time.
  • Extensible: Easily inject custom parsers, renderers, or overlap resolvers.

Installation

composer require iceylan/subtitle

Quick Start

Convert a SubRip (.srt) file into a WebVTT (.vtt) string while automatically cleaning up time overlaps and normalizing lines in one single chain:

use Iceylan\Subtitle\Parse;
use Iceylan\Subtitle\Renderers\VTT;

// Read SRT, sanitize, clean overlaps, and output as WebVTT string
$content = Parse::from( 'movie.srt' )
    ->sanitize()
    ->resolveOverlaps()
    ->render( VTT::class );

file_put_contents( 'movie.vtt', $content );

Architecture & Core Concepts

1. Unified Gateway (Parse)

The Parse class acts as a static gateway that utilizes lazy-loading and driver detection. It normalizes heterogeneous line endings (\r\n, \r) to unified unix lines (\n) under the hood before delegating the work to the appropriate parser.

use Iceylan\Subtitle\Parse;

// Auto-detects and loads the file into a Collection
$collection = Parse::from( 'path/to/subtitle.srt' );

Registering Custom Formats

You can expand the library by adding your own parser drivers without touching the core source code:

Parse::register( MyCustomParserDriver::class );

2. Deep Immutable Engine (Collection & Entry)

Every manipulation creates a brand-new timeline universe. Thanks to deep-cloning capability, original instances remain unpolluted.

$original = Parse::from( 'movie.srt' );

// Each line creates an independent variation of your timeline
$shifted   = $original->delay( 2500 ); // 2.5 seconds forward
$converted = $original->changeFPS( 23.976, 25.0 ); 

// $original remains completely untainted in memory!

Complete API Reference

Collection Queries

get

get(int $index): Entry

Retrieves a specific subtitle cue entry. Supports negative indexes to seamlessly grab entries counting back from the end of the timeline.

$firstEntry = $collection->get( 0 );
$lastEntry  = $collection->get( -1 ); // Python-style reverse indexing!

Or you can also use the subtitle collection like an array:

$firstEntry = $collection[ 0 ];
$lastEntry  = $collection[ -1 ]; // Python-style reverse indexing!

getEntries

getEntries(): array

Extracts the underlying raw array containing all Entry instances.

$rawArray = $collection->getEntries();

between

between(int $startMs, int $endMs): Collection

Retrieves subtitle cues between two timestamps in milliseconds.

$timeWindow = $collection->between( 15000, 80000 );
// between first 15 and 80 seconds

until

until(int $endMs): Collection

Retrieves subtitle cues until a specific timestamp in milliseconds.

$firstTenSeconds = $collection->until( 10000 );

since

since(int $startMs): Collection

Retrieves subtitle cues since a specific timestamp in milliseconds.

$lastTenSeconds = $collection->since( 10000 );

count

count(): int

Returns the total number of subtitle entries inside the collection.

$totalCues = $collection->count();

Or you can also use the subtitle collection like an array:

$totalCues = count( $collection );

duration

duration(): int

Calculates the gross chronological timeline span between the very first starting cue and the absolute last ending cue in milliseconds.

$totalMovieTimeSpan = $collection->duration();

characters

characters(): int

Returns the total number of characters across all subtitle cues.

$totalCharacters = $collection->characters();
// 75489

words

words(): int

Returns the total number of words across all subtitle cues.

$totalWords = $collection->words();
// 12295

screenTime

screenTime(): int

Calculates the absolute summation of individual subtitle visibility durations in milliseconds.

$activeMilliseconds = $collection->screenTime();

silentTime

silentTime(): int

Calculates the absolute summation of individual subtitle silence durations in milliseconds.

$inactiveMilliseconds = $collection->silentTime();

silenceDensity

silenceDensity(): float|int

Returns the ratio of the silent duration to the duration the text remains on the screen.

$silenceRatio = $collection->silenceDensity();
// 0.2485

talkativeDensity

talkativeDensity(): float|int

Returns the ratio of the time text remains hidden on the screen to the total visible time.

$silenceRatio = $collection->talkativeDensity();
// 4.2042

avgDurationPerEntry

avgDurationPerEntry(): float|int

Returns the average time (in milliseconds) each entry appears on the screen.

$averageDuration = $collection->avgDurationPerEntry();

avgSilenceBetweenEntries

avgSilenceBetweenEntries(): float|int

Returns the average silence duration (in milliseconds) between subtitle cues.

$averageSilence = $collection->avgSilenceBetweenEntries();
// 731.3

avgCharsPerSecond

avgCharsPerSecond(): float|int

Returns the number of characters per second. This can be considered the average speed of the subtitles.

$averageCharactersPerSecond = $collection->avgCharsPerSecond();
// 13.29

avgWordsPerMinute

avgWordsPerMinute(): float|int

Returns the number of words per minute.

$averageWordsPerMinute = $collection->avgWordsPerMinute();
// 130.61

Collection Transformations

delay

delay(int $ms): Collection

Shifts the entire subtitle track timeline forward or backward by the given amount of milliseconds.

$twoSecsForward = $collection->delay( 2000 );
$oneSecBackward = $collection->delay( -1000 );

delayFrom

delayFrom(int $fromIndex, int $ms): Collection

Shifts timelines by milliseconds exclusively for entries starting from a specific index boundary up to the end.

// Shifts everything starting from index 15 and onwards by 3 seconds
$partiallyShifted = $collection->delayFrom( 15, 3000 );

cut

cut(int $fromIndex, int $length = 1): Collection

Excises a specific range of subtitle cues out of the collection and automatically retrofits the chronologically succeeding blocks back in time to heal the gap.

$trimmedCollection = $collection->cut(from: 10, length: 3);

sanitize

sanitize(): Collection

Loops through all textual contents of the cues and strips out any untrusted markup or HTML tags.

$cleanTextCollection = $collection->sanitize();

changeFPS

changeFPS(float $from, float $to): Collection

Re-calculates all timestamp sequences proportionally to match target frame rate transitions.

$palVersion = $collection->changeFPS( 23.976, 25.0 );

stretch

stretch(int $srcAnchor1, int $desAnchor1, int $srcAnchor2, int $desAnchor2): Collection

Applies linear time interpolation scaling across the entire timeline using two custom real-time reference anchor coordinates. Perfect for syncing subtitles when the frame rate is unknown.

$syncedCollection = $collection->stretch(
    srcAnchor1: 5000,  desAnchor1: 7200,  // first reference point warp
    srcAnchor2: 90000, desAnchor2: 94500  // second reference point warp
);

filter

filter(Closure $callback): Collection

Filters the collection using a custom boolean evaluation closure. Returns a brand new subset collection.

$adsRemoved = $collection->filter( function( Entry $entry )
{
    return ! in_array( '[www.yourads.com](https://www.yourads.com)', $entry->content );
});

map

map(Closure $callback): Collection

Applies a custom callback to each entry in the collection. Returns a brand new subset collection.

$toUpperCase = fn( Entry $entry ) => $entry->content = strtoupper( $entry->content );
$upperCased = $collection->map( $toUpperCase );

If the callback returns anything other than an Entry object, neither that entry nor the returned value will be added to the final collection.

each

each(Closure $callback): Collection

Applies a custom callback to each entry in the collection. Returns a brand new collection.

$entry10 = $collection->get( 10 );

$collection->each( function( Entry $entry, int $index )
{
    if( $index === 10 )
    {
        if( $entry === $entry10 )
        {
            // this line never gets executed
        }
        else 
        {
            exit(
                'The $entry object cannot be modified because it is '.
                'read-only while inside the immutable method'
            );
        }
    }
});

slice

slice(int $startIndex, ?int $length = null): Collection

Extracts a designated window slice out of the collection sequence.

$firstTenLines = $collection->slice(0, 10);

merge

merge(Collection $other): Collection

Merges two separate collections together and automatically re-sorts them chronologically.

$fullSubtitles = $partOneCollection->merge($partTwoCollection);

append

append(Collection $other, int $gapMs = 0): Collection

Appends one collection to the end of another. Optionally adds a safe gap between the two collections. It can be useful for example when you have two separate subtitle tracks that need to be merged together like CD1 and CD2.

$CD1 = Parse::from( "movie/cd-1.srt" ); // items 500
$CD2 = Parse::from( "movie/cd-2.srt" ); // items 200

$fullSubtitles = $CD1->append( $CD2, 1000 );

echo $CD1->count(); // 500
echo $fullSubtitles->count(); // 700

push

push(Entry $entry) / addEntry(Entry $entry): Collection

Safely appends a new subtitle element block into the timeline pool and auto-arranges its chronological index placement.

use Iceylan\Subtitle\Entry;

$newCue = ( new Entry )
    ->setSequenceNumber( 101 )
    ->setStart( 12000 )
    ->setEnd( 15500 )
    ->addContent( "Breaking News!" );

$updatedCollection = $collection->push( $newCue );

resolveOverlaps

resolveOverlaps(?OverlapsResolverInterface $resolver = null): Collection

Resolves temporal overlapping collision structural errors where a trailing block's start overlaps a preceding block's end. Uses a default TrimAndGapResolver(10) if no strategy is specified.

// Clean up overlaps leaving a safe 10ms gap between colliding items
$resolved = $collection->resolveOverlaps();

Overlap Resolution Strategies

You can swap out the collision resolution strategy or build your own by implementing OverlapsResolverInterface.

Built-in Strategy: TrimAndGapResolver

This strategy adjusts the end time of the preceding item to create a comfortable gap before the next subtitle block arrives, safely handling narrow intervals or empty text fallbacks.

use Iceylan\Subtitle\Resolvers\TrimAndGapResolver;

// Enforces a strict 20ms gap between any overlapping blocks
$resolved = $collection->resolveOverlaps( new TrimAndGapResolver( 20 ));

Network Optimization & Frontend Integration

Both Entry and Collection natively implement JsonSerializable. The JSON payload keys are highly compressed to reduce data overhead, making it ideal for directly streaming files over the wire to your Vue.js, React, or modern web video players.

echo json_encode( $collection );

Compressed JSON Output Structure:

[
  {
    "sq": 1,
    "ms": [ "Hello, welcome to the film." ],
    "st": 1500,
    "en": 4200
  },
  {
    "sq": 2,
    "ms": [ "Subtitles optimized for network speed." ],
    "st": 4500,
    "en": 8000
  }
]
  • sq: Sequence Number
  • ms: Message / Text Array content lines
  • st: Start timestamp in absolute milliseconds
  • en: End timestamp in absolute milliseconds

Extending with Custom Formats

Adding support for an extra subtitle format is extremely straight-forward. Simply implement the core interfaces provided under the Support layer.

Creating a Custom Parser Driver

namespace App\Subtitle\Parsers;

use Iceylan\Subtitle\Collection;
use Iceylan\Subtitle\Support\ParserInterface;

class MyCustomParser implements ParserInterface
{
    public static function canParse( string $content, string $extension ): bool
    {
        return $extension === 'custom';
    }

    public function parse( string $content ): Collection
    {
        $collection = new Collection();
        // Custom parsing state logic here...
        return $collection;
    }
}

Contributing

I welcome all kind of contributions!

The infrastructure is built using the Infrastructure Strategy Pattern. To add a new format, simply write a class that implements ParserInterface and RendererInterface, place them in the appropriate directories, register them in the Parser class, and submit a PR, that’s all you need to do!

Upcoming Planned Features

I am committed to making this package the ultimate Subtitle Swiss Army Knife for PHP. Here are the enterprise-grade concepts and features currently in active planning. If you want to contribute to any of these, feel free to open an Issue or PR!

  • Subtitle Validator
    • An automated quality control engine to validate structural correctness before rendering or uploading.
    • Features: Check for empty cue blocks, flag hyper-short durations (e.g., cues under 300ms), detect character-per-line (CPL) threshold violations, and warn if an entry exceeds industry-standard safe line counts (e.g., max 2 lines).
  • Smart HTTP Output & Streaming Drivers
    • Seamless integration out of the box for modern frameworks like Laravel and Symfony to handle file delivery.
    • Features: Direct-to-browser attachment download wrappers, automated server-side content-type handling (text/vtt, application/x-subrip), and memory-optimized stream transmitters for massive subtitle files.
  • Bulletproof Encoding & Charset Gateway
    • Auto-remediation gateway integrated directly into the Parse::from() workflow to end encoding nightmares once and for all.
    • Features: Automatic runtime detection of legacy charsets (like Windows-1254, ISO-8859-9, UTF-16) and transparent background translation into pure, uncorrupted UTF-8 before parsing begins. Perfect for reliable localization.
  • Advanced Typography & Rich Document Object Model (DOM)
    • Moving away from flat string arrays to an isolated, robust typography and word-granularity ecosystem without breaking backward compatibility or complicating simple formats like SRT.
    • Hierarchical Components:
      • Content\Style: A standalone object housing explicit format properties like bold, italic, underline, and color codes (e.g., hex, named colors). Can be attached globally to an entire cue or down to individual tokens.
      • Content\Document: Replaces the raw text array inside Entry::$content but implements ArrayAccess, Countable, and IteratorAggregate to act exactly like an array from the outside.
      • Content\Line: Represents a single line within a subtitle block, managing an isolated collection of structural words/tokens.
      • Content\Word: The atomic unit of text. Houses individual token values, custom word-level styling, and temporal offsets relative to the main cue's timeline (essential for implementing state-of-the-art Karaoke / Word-by-Word highlighting modes like in advanced WebVTT/ASS workflows).

License

This library is released under the MIT License.

统计信息

  • 总下载量: 0
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 3
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2026-06-15

承接程序开发

PHP开发

VUE

Vue开发

前端开发

小程序开发

公众号开发

系统定制

数据库设计

云部署

网站建设

安全加固