Skip to content

Commit

Permalink
feat(h2non#16): support ass/ssa subtitles
Browse files Browse the repository at this point in the history
  • Loading branch information
h2non committed Mar 18, 2015
1 parent 26f62e4 commit 5bb2657
Show file tree
Hide file tree
Showing 12 changed files with 241 additions and 42 deletions.
17 changes: 6 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,8 @@ Default options are:
- **captionStart** `number` - Miliseconds to start the caption. Default to `1000`
- **captionEnd** `number` - Miliseconds to remove the caption. Default to `loop - 1000`
- **logo** `string` - Path to logo image. See `logo()` method
- **useSubRipSubtitles** `boolean` - Use SubRip subtitles format. It uses by default [SSA/ASS](http://en.wikipedia.org/wiki/SubStation_Alpha). Default `false`
- **subtitlesStyle** `object` - SSA/ASS subtitles style. See [substation.js](https://github.com/h2non/videoshow/blob/master/lib/substation.js) and [fixture file](https://github.com/h2non/videoshow/blob/master/test/fixtures/subtitles.ass) for examples
#### videoshow#image(image)
Expand All @@ -204,7 +206,7 @@ It supports multiple formats and codecs such as `acc`, `mp3` or `ogg`
#### videoshow#logo(path [, params ])
Add a custom image as logo in the left-upper corner.
Add a custom image as logo in the left-upper corner by default. You can customize the position by `x/y` axis
It must be a `png` or `jpeg` image
**Supported params**:
Expand All @@ -216,17 +218,10 @@ It must be a `png` or `jpeg` image
#### videoshow#subtitles(path)
Define the [SubRip subtitles][subrip]
file path to load. It should be a `.srt` file
Define the [SubRip subtitles][subrip] or [SubStation Alpha](http://en.wikipedia.org/wiki/SubStation_Alpha) (SSA/ASS)
file path to load. It should be a `.str` or `.ass` file respectively
If you want to use [SubStation Alpha](http://en.wikipedia.org/wiki/SubStation_Alpha) (SSA/ASS) subtitles,
you should pass it as video input:
```js
videoshow(images)
.input('subtitles.ass')
.save('video.mp4')
```
See [fixtures](https://github.com/h2non/videoshow/blob/master/test/fixtures/subtitles.srt) for examples
#### videoshow#save(path)
Return: `EventEmitter` Alias: `render`
Expand Down
11 changes: 10 additions & 1 deletion examples/captions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@ var videoshow = require('../')
var audio = __dirname + '/../test/fixtures/song.mp3'

var options = {
transition: true
transition: true,
useSubRipSubtitles: false,
subtitleStyle: {
Fontname: 'Verdana',
Fontsize: '24',
PrimaryColour: '11861244'
}
}

var images = [
Expand Down Expand Up @@ -31,6 +37,9 @@ var images = [
videoshow(images, options)
.audio(audio)
.save('video.mp4')
.on('start', function (command) {
console.log('ffmpeg process started:', command)
})
.on('error', function (err) {
console.error('Error:', err)
})
Expand Down
3 changes: 2 additions & 1 deletion examples/subtitles.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
var videoshow = require('../')

var subtitles = __dirname + '/../test/fixtures/subtitles.srt'
var subtitles = __dirname + '/../test/fixtures/subtitles.ass'
//var subtitles = __dirname + '/../test/fixtures/subtitles.srt'
var audio = __dirname + '/../test/fixtures/song.mp3'

var images = [
Expand Down
28 changes: 28 additions & 0 deletions lib/mstime.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
var measures = [ 3600000, 60000, 1000 ]

var msTime = module.exports = function (val, leadingZeros) {
leadingZeros = leadingZeros || 2

var time = measures.map(function (measure) {
var res = (val / measure >> 0).toString()
if (res.length < 2) {
res = '0' + res
}
val %= measure
return res
})

var ms = val.toString()
if (ms.length < 3) {
for (var i = 0, l = ms.length; i <= leadingZeros - l; i += 1) {
ms = '0' + ms
}
}

return time.join(':') + ',' + ms
}

msTime.substation = function (val) {
var time = msTime(val, true)
return time.replace(',', '.').slice(1)
}
4 changes: 3 additions & 1 deletion lib/options.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ var defaults = exports.defaults = {
size: '640x?',
audioBitrate: '128k',
audioChannels: 2,
format: 'mp4'
format: 'mp4',
useSubripSubtitles: false,
subtitleStyle: null
}

exports.define = function (options) {
Expand Down
19 changes: 14 additions & 5 deletions lib/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ var ffmpeg = require('fluent-ffmpeg')
var EventEmitter = require('events').EventEmitter

var subrip = require('./subrip')
var substation = require('./substation')
var copy = require('./copy')
var video = require('./video')
var merge = require('./merge')
Expand Down Expand Up @@ -209,8 +210,9 @@ function logoFilter(videoshow) {
function getImageSubtitles(videoshow) {
var filepath = null
var params = options.define(videoshow.params)
var length = 0
var extension = '.ass'

var length = 0
var subtitles = videoshow.images
.map(function (image, index) {
var offset = calculateOffsetDelay(index)
Expand All @@ -221,13 +223,20 @@ function getImageSubtitles(videoshow) {
return caption != null
})

subtitles = subrip.stringify(subtitles)
if (!subtitles.length) {
return
}

if (subtitles) {
filepath = randomName() + '.srt'
fs.writeFileSync(filepath, subtitles)
if (params.useSubRipSubtitles) {
extension = '.srt'
subtitles = subrip.stringify(subtitles)
} else {
subtitles = substation.stringify(subtitles, params.subtitleStyle)
}

filepath = randomName() + extension
fs.writeFileSync(filepath, subtitles)

return filepath

function calculateOffsetDelay(index) {
Expand Down
26 changes: 3 additions & 23 deletions lib/subrip.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
var measures = [ 3600000, 60000, 1000 ]
var msTime = require('./mstime')

exports.stringify = function (data) {
var buf = data.map(function (s) {
var track = []

if (!isNaN(s.startTime) && !isNaN(s.endTime)) {
s.startTime = msTime(parseInt(s.startTime, 10))
s.endTime = msTime(parseInt(s.endTime, 10))
s.startTime = msTime(+s.startTime)
s.endTime = msTime(+s.endTime)
}

track.push(s.id)
Expand All @@ -18,23 +18,3 @@ exports.stringify = function (data) {

return buf.join('\n\n')
}

function msTime(val) {
var time = measures.map(function (measure) {
var res = (val / measure >> 0).toString()
if (res.length < 2) {
res = '0' + res
}
val %= measure
return res
})

var ms = val.toString()
if (ms.length < 3) {
for (var i = 0, l = ms.length; i <= 2 - l; i += 1) {
ms = '0' + ms
}
}

return time.join(':') + ',' + ms
}
123 changes: 123 additions & 0 deletions lib/substation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
var substation = require('ass-stringify')
var merge = require('lodash.merge')
var msTime = require('./mstime')

var subtitleStyle = {
'Marked': 'Marked=0',
'Start': '0:00:01.18',
'End': '0:00:06.85',
'Style': 'DefaultVCD',
'Name': 'NTP',
'MarginL': '0000',
'MarginR': '0000',
'MarginV': '0000',
'Effect': null,
'Text': null
}

var defaultParams = {
'Script Info': {
'Title': 'Videoshow video',
'ScriptType': 'v4',
'Collisions': 'Normal',
'PlayResY': '600',
'PlayDepth': '0',
'Timer': '100,0000'
},
'Empty': {},
'V4 Styles': {
'Format': [
'Name',
'Fontname',
'Fontsize',
'PrimaryColour',
'SecondaryColour',
'TertiaryColour',
'BackColour',
'Bold',
'Italic',
'BorderStyle',
'Outline',
'Shadow',
'Alignment',
'MarginL',
'MarginR',
'MarginV',
'AlphaLevel',
'Encoding'
],
'Style': {
'Name': 'DefaultVCD',
'Fontname': 'Arial',
'Fontsize': '28',
'PrimaryColour': '11861244',
'SecondaryColour': '11861244',
'TertiaryColour': '11861244',
'BackColour': '-2147483640',
'Bold': '-1',
'Italic': '0',
'BorderStyle': '1',
'Outline': '1',
'Shadow': '2',
'Alignment': '2',
'MarginL': '30',
'MarginR': '30',
'MarginV': '30',
'AlphaLevel': '0',
'Encoding': '0'
}
},
'Events': {
'Format': [
'Marked',
'Start',
'End',
'Style',
'Name',
'MarginL',
'MarginR',
'MarginV',
'Effect',
'Text'
]
}
}

exports.stringify = function (captions, styles) {
var params = mergeParams(styles)

captions.forEach(function (caption) {
var subtitle = merge({}, subtitleStyle, {
Start: msTime.substation(+caption.startTime),
End: msTime.substation(+caption.endTime),
Text: caption.text
})

var body = params[params.length - 1].body
body.push({
key: 'Dialogue',
value: subtitle
})
})

return substation(params)
}

function mergeParams(styles) {
var params = merge({}, defaultParams)
merge(params['V4 Styles'].Style, styles)

return Object.keys(params).map(function (key) {
var body = params[key]
var map = { section: key }

map.body = Object.keys(body).map(function (prop) {
return {
key: prop,
value: body[prop]
}
})

return map
})
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"slideshows"
],
"dependencies": {
"ass-stringify": "^0.1.3",
"fluent-ffmpeg": "^2.0.0-rc3",
"fw": "^0.1.2",
"lil-uuid": "^0.1.0",
Expand Down
21 changes: 21 additions & 0 deletions test/fixtures/subtitles.ass
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[Script Info]
; This is a Sub Station Alpha v4 script.
; For Sub Station Alpha info and downloads,
; go to http://www.eswat.demon.co.uk/
Title: Neon Genesis Evangelion - Episode 26
Original Script: RoRo
Collisions: Normal
PlayDepth: 0
Timer: 100,0000

[Empty]

[V4 Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, TertiaryColour, BackColour, Bold, Italic, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, AlphaLevel, Encoding
Style: DefaultVCD,Verdana,12,11861244,11861244,11861244,-2147483640,-1,0,1,1,2,2,30,30,30,0,0

[Events]
Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text
Dialogue: Marked=0,0:00:01.18,0:00:06.85,DefaultVCD,NTP,0000,0000,0000,,{\pos(400,570)}Like an angel with pity on nobody
Dialogue: Marked=0,0:00:07.00,0:00:12.00,DefaultVCD,NTP,0000,0000,0000,,Hey this is another text Hey this is another text Hey this is another text Hey this is another text Hey this is another text,Hey this is another text,Hey this is another text
Dialogue: Marked=0,0:00:13.00,0:00:16.00,DefaultVCD,NTP,0000,0000,0000,,Hello world
30 changes: 30 additions & 0 deletions test/substation.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
var expect = require('chai').expect
var substation = require('../lib/substation')

suite('substation', function () {
suite('stringify', function () {
test('single', function () {
var subtitle = {
id: 1,
startTime: 1000,
endTime: 3000,
text: 'Hello World'
}

var params = {
Fontname: 'Verdana',
Fontsize: '28',
PrimaryColour: '11861244'
}

var lines = substation.stringify([ subtitle ], params).split('\n')
lines.pop()

expect(lines.shift()).to.be.equal('[Script Info]')
expect(lines.shift()).to.be.equal('Title: Videoshow video')
expect(lines.shift()).to.be.equal('ScriptType: v4')
expect(lines.pop()).to.be.equal('Dialogue: Marked=0,0:00:01.00,0:00:03.00,DefaultVCD,NTP,0000,0000,0000,,Hello World')
expect(lines.pop()).to.be.equal('Format: Marked, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text')
})
})
})
Binary file added video.mp4
Binary file not shown.

0 comments on commit 5bb2657

Please sign in to comment.