ビデオに字幕を表示する

ビデオに字幕を表示する方法を探していて、あちこちでいろいろな情報を得た後になんとかそれっぽいことができるようになったのでメモ。ソースがどこだか忘れてしまったので、思い出したらそれも追加する。

元々は、作っているアプリケーションで QTKit でそれっぽいことをしていたんだけど、いろいろ限界もあるのと 10.9 で QTKit が deprecated になってしまったので、AVFoundation と Core Animation で何とかならないかと思って、たどり着いたところ。そのアプリケーションの物を引きずっているので、動画の表示サイズと同じ NSImage オブジェクトを作って、それを動画に重ねるという形になっている。いずれは、字幕部分だけを描いて表示させたいが、それは、また時間ができたときの話。

基本的には、QTKit でのタイムコードの形式の [00:00:00.000] で時間指定がしてあるものとして、タイムスケールは 1000 (1/1000 秒単位を扱う)としてある。

感じとしては、

[00:00:00.000]

ここに字幕のテキストを入れる。

[00:00:01.500]

という形式で、0 から 1.5 秒の間で字幕が表示されることになる。

テキストは @textView という NSTextView にあって、それを読み込む形を取る。

メモを入れながらのスクリプトが見にくいかもしれないので、最後にスクリプトだけをまとめた物を付けてある。

# まずは、起動したとき、もしくはどこかでビデオファイルを読み込む。細かいことは、AVPlayerView を使ってメディアファイルを表示あたりを参照してください。

def awakeFromNib

asset = AVAsset.assetWithURL(url)

playerItem = AVPlayerItem.alloc.initWithAsset(asset)

avplayer = AVPlayer.playerWithPlayerItem(playerItem)

@playerView.player = avplayer

# タイムスケールは 1000 にしてある。ここを 600 にすると、QuickTime のテキストトラックのデータが使えるはず。

@timeScale = 1000

end

# ここからが、字幕を追加する処理。上の AVPlayerView の layer に描いて同期させている。

def addSubtitle(sender)

# まずは、以前に追加した字幕 layer があればそれを取り除く。

# 字幕は、AVSynchronizedLayer の sub layer として追加しているので、AVSynchronizedLayer ごと削除する。

@playerView.layer.sublayers.each do |layer|

if layer.class == AVSynchronizedLayer

layer.removeFromSuperlayer

break

end

end

# 映像ファイルの長さを取り指している。取り出した値は CMTime になっている。CMTime に関しては、CMTimeを参照してみてください。ここでは細かい説明は省きます。

durationTime = @playerView.player.currentItem.duration

# 字幕の時間とテキストの情報は @textView にある物とするが、最初の字幕までと最後の字幕からの部分も必要なので、最後のタイムコードを作りテキスト情報に追加する。もっとスマートな方法はないものか。

terminalTimeSec = CMTimeConvertScale(durationTime, @timeScale ,KCMTimeRoundingMethod_RoundHalfAwayFromZero).value * 1.0 / @timeScale

hourVal = sprintf("%02d",(terminalTimeSec/3600).to_i)

minVal = sprintf("%02d", terminalTimeSec % 3600 / 60)

secVal = sprintf("%02d", terminalTimeSec % 3600 % 60 / 1)

fracVal = sprintf("%03d", terminalTimeSec % 3600 % 60 % 1 * @timeScale)

terminalTimeStamp = "#{hourVal}:#{minVal}:#{secVal}.#{fracVal}"

# それぞれの字幕のテキストと時間情報を保存するための配列を作っておく。

# ついでに、字幕の表示開始時間と、字幕のイメージを保存する配列も用意。

subTArray = Array.new

subTImages = Array.new

subtitleTimes = Array.new

subTImages = Array.new

# 字幕の文字の大きさと、画面したからの位置(offset)、字幕が 2 行以上あるときはその行間(lineSpace)。

# あと、字幕にアウトラインを付けたいので、その太さ(outlineWidth)を指定している。

# offset は、playerView のサイズ変更に伴って、字幕の大きさを変えるときに参照したいので、インスタンス変数にしておく。

fontSize = 22.0

@offset = 10

lineSpace = 5

outlineWidth = 7

# テキストは、NSMutableAttributedString でつくる(NSAttributedString でもいいかも)ので、その属性をここで指定。色は黄色にしてあるが、好みの色に変更可。

# フォントは Helvetica にしているが、これも変更可。

textAttributes = Hash.new

textAttributes[NSForegroundColorAttributeName] = NSColor.yellowColor

textAttributes[NSFontAttributeName] = NSFont.fontWithName("Helvetica",size:fontSize)

# 字幕を表示するエリアの大きさを決めるために最大の高さと横幅を記録する変数を用意。

# サイズ変更の際に参照したいので、インスタンス変数で用意する。

@maxHeight = 0

@maxWidth = 0

# @textView のテキストの最初と最後にタイムコードを入れて、タイムコードを String#scan で処理していく。

"[00:00:00.000]\n#{@textView.string}\n[#{terminalTimeStamp}]\n".scan(/\[((\d+?):(\d+?):(\d+?)\.(\d+))\]/) do |stamp|

# おまじない。というか、最後のタイムコード後を処理しないようにタイムコード後にテキストがない場合は次にいく(break でもいいかも)。

next if $'.nil?

# 始まりのタイムコードを得て、それから CMTime を作っている。$2 ~ $5 としているが、stamp[1] ~ stamp[4] でも同じはず。

timeStamp = stamp[0]

initTime = ($2.to_i * 3600 + $3.to_i * 60 + $4.to_i) * @timeScale + $5.to_i

startCMTime = CMTimeMakeWithSeconds(initTime/@timeScale,durationTime.timescale)

# ここでも、もう一度チェック。最後のタイムコード以降にテキストがあったとしても、タイムコードがなければ処理しないようにしている。

next if $'.match(/\[((\d+?):(\d+?):(\d+?)\.(\d+))\]/).nil?

# 次のタイムコードを検索しているので、そのタイムコードの間にある文字列が字幕の文字として扱われることになる。

subtitle = $`.to_s.strip

# ここでは、字幕表示終了のタイムコードを得て、CMTime をつくっている。

# また、表示終了のタイムコードが始まりのタイムコードと同じなら、処理しないで次へ行く。

endTimeStamp = $1

next if timeStamp == endTimeStamp

endTime = ($2.to_i * 3600 + $3.to_i * 60 + $4.to_i) * @timeScale + $5.to_i

endCMTime = CMTimeMakeWithSeconds(endTime/@timeScale,durationTime.timescale)

# 字幕は、一行ごとに処理するので、まずは、改行文字で分割。

subTTexts = subtitle.split(/\r?\n|\r/)

# 字幕の文字列がある場合のみ、NSMutableAttributedString に変換して、最大の高さ・横幅の値を更新していく。

if subTTexts != []

subTTexts.map!{|x| NSMutableAttributedString.alloc.initWithString(x)}

subTTexts.map!{|x| x.addAttributes(textAttributes,range:[0,x.length]) }

subTTexts.map!{|x| x.setAlignment(NSCenterTextAlignment,range:[0,x.length])}

# inject で、行の分だけテキストの高さを加えていく。

# その際は、文字の高さ、幅に outline の幅を加えた物に、lineSpace を加えていく。

subTHeight = subTTexts.inject(0){|sum,attrString| sum + attrString.size.height + outlineWidth * 2} + (subTTexts.length - 1) * lineSpace

@maxHeight = subTHeight if subTHeight > @maxHeight

subTWidth = subTTexts.map{|x| x.size.width + outlineWidth * 2}.max

@maxWidth = subTWidth if subTWidth > @maxWidth

end

parentLayer.sublayers.each do |layer|

@playerLayer = layer if layer.class == NSKVONotifying_AVPlayerLayer

end

# 上に書いたように、parentLayer に synchLayer を追加するとコントローラの手前に字幕が出てしまうので、次のように下。

# ハッシュを作り、字幕の配列と、表示開始時間、表示する時間(長さ: duration)を記録していく。

subtitleHash = Hash.new

subtitleHash["subtitle"] = subTTexts

subtitleHash["startCMTime"] = startCMTime

subtitleHash["durationCMTime"] = CMTimeSubtract(endCMTime,startCMTime)

subTArray << subtitleHash

end

# 上で作った字幕の情報が入った配列をブロックで処理。

subTArray.each_with_index do |subT,idx|

# NSImage の imageWithSize:flipped:drawingHandler で NSImage オブジェクトを作る。

# イメージの表示領域は、高さ、横幅の最大値を取っている。

image = NSImage.imageWithSize([@maxWidth,@maxHeight],flipped:false,drawingHandler:Proc.new{|handler|

# AVFoundation の所にある波形表示のサンプルと違って、ここでは Core Graphics で描画している。

# まずは、表示するコンテクストの用意。NSGraphicsContext で用意している(CGGraphicsContext ってあるのか?)。RubyMotion では、最後に、to_object を付けないとちゃんと動かない(これがずっとわからなかった)。

context = NSGraphicsContext.currentContext.graphicsPort.to_object

# 塗りつぶしの色の指定。字幕以外は透明にしたいので、NSColor.clearColor で作り、それを CGColor に変換している。

# そして、CGContextFillRect で、指定した Rect を塗りつぶす。

CGContextSetFillColorWithColor(context, NSColor.clearColor.CGColor)

CGContextFillRect(context, CGRectMake(0,0,@maxWidth,@maxHeight))

# 次に、上で準備した字幕テキストを SubTs という変数を作って入れておく

# 多分、いちいち作らなくても動くと思うが、念のため。

subTs = subT['subtitle']

# lineNum で何行あるかというのを取り出しているのは、表示位置決めのため。

lineNum = subTs.length

# ブロックで処理していく。

subTs.each_with_index do |subTitle,idx2|

# まずは、NSMutableAttributedString オブジェクトを作り、全範囲に対して、上で指定した属性を付ける。

# また、alignment を中央にしているが、今の方法では実は意味がない。ただ、将来のために残しておく。

# alignment を中央にしてはいるが、実はあまり関係ないことに気づいたので、作った NSMutableAttributedString オブジェクトの横幅を表示域の横幅(最大値)から引いた物を半分にした位置から表示することで、中央表示にしている。

textPosX = (@maxWidth - subTitle.size.width)/2

# ここで、NSMutableAttributedString オブジェクトから Core Text の line オブジェクトを作っている。

# drawAtPoint でも表示できるが、このような面倒くさい処理にしているのは、次に行うアウトラインをきれいに描くため。

line = CTLineCreateWithAttributedString(subTitle)

# 作った CTLine オブジェクトから、Glyph の情報を得て、それを使ってアウトラインを描いている。

# 実はよくわかっていない。この辺りを参考にしたので、当たってみてください。http://qiita.com/nofrmm/items/abd8d47ccf1191f4ba13

# CTLineGetGlyphRuns で Glyph を配列として取り出しているので、それをブロック処理。

# CGPathCreateMutable で CGPath として情報を集めていく準備。

runs = CTLineGetGlyphRuns(line)

letters = CGPathCreateMutable()

runs.each do |run|

# アウトラインのフォントは文字のフォントと同じでないと困るので、Glyph のフォント情報を得ている。

runFont = CTRunGetAttributes(run)['NSFont']

# CTRunGetGlyphCount で Glyph の数を得て、その回数分だけブロック処理。

CTRunGetGlyphCount(run).times do |ridx|

# まずは、一文字分ずつの range を作る。Core Text は CF~ でいろいろと準備しないといけないらしい。

glyphRange = CFRangeMake(ridx, 1);

# Glyph と position の情報は、ポインタを用意する必要があるので、Pointer オブジェクトを作る。

# glyph は 'S' を、position は CGPoint で用意する。

glyph = Pointer.new('S',1)

position = Pointer.new(CGPoint.type,2)

# CTRunGetGlyphs で Glyph の情報を glyph に取り出す。同様に CTRunGetPositions で position の情報を position に取り出す。

CTRunGetGlyphs(run, glyphRange, glyph)

CTRunGetPositions(run, glyphRange, position)

# CTFontCreatePathForGlyph で、取り出した Glyph のパス情報を取り出す。

letter = CTFontCreatePathForGlyph(runFont, glyph[0], nil)

# CGAffineTransform もポインタを用意する必要があるようなので用意した(この辺りはよくわかっていない)。

atf = Pointer.new(CGAffineTransform.type,1)

# ここでは、文字の位置を指定するようなので、上の CTLine を描く位置と同じ位置を指定している。そして、CGPath に path を追加していく。

atf[0] = CGAffineTransformMakeTranslation(position[0].x+textPosX, 8 + (fontSize+lineSpace) * (lineNum - idx2 - 1))

CGPathAddPath(letters, atf, letter)

end

end

# ここから、実際に文字を書いていく。

# まずは CGContextSetTextPosition で位置を指定している。

CGContextSetTextPosition(context,textPosX, 8 + (fontSize+lineSpace) * (lineNum - idx2 - 1))

# CGContextSetLineJoin はアウトラインを繋ぐ方法らしいが、KCGLineJoinRound は滑らかに描くおまじない。CGContextAddPath で描く線をコンテクストに追加。

CGContextSetLineJoin(context,KCGLineJoinRound)

CGContextAddPath(context, letters)

# ここで、アウトラインの色を黒に指定しているので、変更する場合は NSColor で指定するか、CGColor で。

# CGContextSetLineWidth でアウトラインの太さを指定して、CGContextStrokePath で線を描いている。

CGContextSetStrokeColorWithColor(context,NSColor.blackColor.CGColor)

CGContextSetLineWidth(context, outlineWidth)

CGContextStrokePath(context)

# 最後に、アウトラインの上に文字を描いて出来上がり。

CTLineDraw(line, context)

end

})

# 特に名前を指定する必要がここではないが、名前を指定する必要がある場合に備えて(前のスクリプトで必要だったので)残してある。

imageName = "img#{idx}"

image.setName(imageName)

# ここで、NSBitmapImageRep オブジェクトに変換して、これを配列に入れていく。

bitmapRep = NSBitmapImageRep.imageRepWithData(image.TIFFRepresentation)

subTImages << bitmapRep

# 時間の指定は、0.0 から 1.0 の間でするので、開始時間を動画ファイルの長さで割っている。

subtitleTimes << subT['startCMTime'].value.to_f / durationTime.value

end

# 字幕を表示するのに CALayer に字幕のイメージを表示するようにするが、映像と同期しないと困るので、AVSynchronizedLayer を使う。

# これで同期させたい AVPlayerView の AVPlayerItem を指定して AVSynchronizedLayer オブジェクトを作る。

synchLayer = AVSynchronizedLayer.synchronizedLayerWithPlayerItem(@playerView.player.currentItem)

# フレームの大きさは、AVPlayerView と同じにして、リサイズするように指定してみているが、思うようにいっていない(字幕自体がリサイズしない。)。この辺りはいずれどうにかしたい。

# setMasksToBounds は中身が bound の外に行くかどうかを決めるらしいが、試していないので、false にするとどうなるかよくわかっていない。

synchLayer.setFrame(@playerView.frame)

synchLayer.setAutoresizingMask(KCALayerWidthSizable | KCALayerHeightSizable)

synchLayer.setMasksToBounds(true)

# AVSynchronizedLayer の sublayer として実際に字幕画像を表示する layer を CALayer で作る。サイズは、AVPlayerView のサイズと同じにしてある。

# サイズ変更の際に参照したいので、インスタンス変数として用意。

# layer のフレームは、ビデオの横幅としているが、これは表示する playerView をビデオのサイズに合わせているから。そうでない場合は、playerView の幅に合わせた方がいいかも。

@overlayLayer = CALayer.layer

@overlayLayer.frame = CGRectMake((@videoSize.width - @maxWidth)/2, @offset, @maxWidth, @maxHeight)

@overlayLayer.setMasksToBounds(true)

# ここまでで作ったイメージデータを CAKeyframeAnimation を使って映像に重ねる。

# keypath は、イメージデータを扱うので、'contents' を指定するようだ。その辺りについては、アップルのデベロッパドキュメントで確認してください。 https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/CoreAnimation_guide/CreatingBasicAnimations/CreatingBasicAnimations.html

anim = CAKeyframeAnimation.animationWithKeyPath("contents")

# 開始時間は、KCMTimeZero、つまり CMTime のゼロを指定すると、使ってる Mac が起動した時間ということになるらしいので、AVCoreAnimationBeginTimeAtZero を使う。

# これで、動画の開始時間と同期する。長さは、動画の長さを指定する。

anim.beginTime = AVCoreAnimationBeginTimeAtZero

anim.duration = CMTimeGetSeconds(durationTime)

# values に、上で作った イメージデータを CGImage にして配列で指定する。そして、keyTimes にそれぞれの字幕の開始時間が 0.0 から 1.0 で指定してある配列の subtitleTimes を指定する。

anim.values = subTImages.map{|x| x.CGImage}

anim.keyTimes = subtitleTimes

# calculationMode は、連続しないイメージを並べるので、KCAAnimationDiscrete を指定する。これで、それぞれの時間を key として、字幕イメージをキーイメージとした、キーフレームのアニメーション、つまり、キーフレームごとにイメージが切り替わる映像が出来上がる。

anim.calculationMode = KCAAnimationDiscrete

# removedOnCompletion で、false を指定しないと、止まったところで映像が消えてしまう(最初に戻る)ので、ちゃんと指定する。

# fillMode で KCAFillModeForwards を指定すると、停止した時点で最後に表示されていたイメージが表示され続ける。

anim.removedOnCompletion = false

anim.fillMode = KCAFillModeForwards

# これはおまじないだけど、いらないかもしれない。

@overlayLayer.displayIfNeeded

# ここで、作った @overlayLayer という名前の CALayer にアニメーションを追加して、その @overlayLayer を AVSynchronizedLayer に sublayer として追加する。

@overlayLayer.addAnimation(anim,forKey:"contents")

synchLayer.addSublayer(@overlayLayer)

# そして、AVSynchronizedLayer の layer を AVPlayerView の layer に sublayer として追加する。

# じつは、ここで、@playerView の layer に追加すると、コントローラーの上に字幕の layer が表示されてしまう。

parentLayer = @playerView.layer

parentLayer.addSublayer(synchLayer)

# 最後に、サイズ変更の際に参照したいので、playerView の layer の sublayer のなかで、AVPlayerLayer を参照できるようにしておく。

# まあ、Ruby らしく、Array#select なんかを使った方が見栄えがいいんだろうけど。いずれは書き換えるかも。

@playerLayer.addSublayer(synchLayer)

end

# あとは、playerView の乗ったパネル・ウィンドウのサイズが変更された際に、字幕の位置と大きさを調整する部分。

# これを使うためには、playerView の乗ったパネル・ウィンドウの delegate をこのスクリプトを書くクラスに指定する。

def windowDidResize(notification)

window = notification.object

# 既に字幕が追加してある場合のみ処理するために、@overlayLayer があるかどうかを確認。

if @overlayLayer

# 字幕の大きさを調整するために、ビデオが表示されている layer の横幅と、ビデオの実際の横幅の比を取っている。

prop = @playerLayer.videoRect.size.width / @videoSize.width

# 字幕を表示する layer (@overlayLayer) のどこに字幕イメージを表示させるかの調整。

# 横幅は、字幕が中央に来るように、playerView の横幅から字幕表示域の横幅を引いたもの 2 で割った値を求める。

# 高さは、playerView の下からの規定オフセット値を調整している。

widthAdjust = (@playerView.frame.size.width - @playerLayer.videoRect.size.width) / 2

heightAdjust = (@playerView.frame.size.height - @playerLayer.videoRect.size.height) / 2

# CALayer は表示域をスケール(scale)するのが面倒くさそうな感じなので、字幕映像自体を、アニメーションさせる形を取っている。

# そして、その変更した字幕の大きさを元に、それを表示する layer の大きさも変更する。

@overlayLayer.transform = CATransform3DMakeScale(prop,prop, 1)

@overlayLayer.frame = CGRectMake((@playerLayer.videoRect.size.width - @maxWidth * prop)/2 + widthAdjust, @offset * prop + heightAdjust, @maxWidth * prop, @maxHeight * prop)

end

end

これで、映像ファイルを再生すると、字幕が付いた状態で再生されるはず。再生を止めると、その時点での字幕が表示されたままになり、途中から再生すると、その部分の字幕から再生されるはず。

最後にスクリプトだけ。

def awakeFromNib

asset = AVAsset.assetWithURL(url)

playerItem = AVPlayerItem.alloc.initWithAsset(asset)

avplayer = AVPlayer.playerWithPlayerItem(playerItem)

@playerView.player = avplayer

@timeScale = 1000

end

def addSubtitle(sender)

@playerView.layer.sublayers.each do |layer|

if layer.class == AVSynchronizedLayer

layer.removeFromSuperlayer

break

end

end

durationTime = @playerView.player.currentItem.duration

terminalTimeSec = CMTimeConvertScale(durationTime, @timeScale ,KCMTimeRoundingMethod_RoundHalfAwayFromZero).value * 1.0 / @timeScale

hourVal = sprintf("%02d",(terminalTimeSec/3600).to_i)

minVal = sprintf("%02d", terminalTimeSec % 3600 / 60)

secVal = sprintf("%02d", terminalTimeSec % 3600 % 60 / 1)

fracVal = sprintf("%03d", terminalTimeSec % 3600 % 60 % 1 * @timeScale)

terminalTimeStamp = "#{hourVal}:#{minVal}:#{secVal}.#{fracVal}"

subTArray = Array.new

subTImages = Array.new

fontSize = 22.0

@offset = 10

lineSpace = 5

outlineWidth = 7

textAttributes = Hash.new

textAttributes[NSForegroundColorAttributeName] = NSColor.yellowColor

textAttributes[NSFontAttributeName] = NSFont.fontWithName("Helvetica",size:fontSize)

@maxHeight = 0

@maxWidth = 0

"[00:00:00.000]\n#{@textView.string}\n[#{terminalTimeStamp}]\n".scan(/\[((\d+?):(\d+?):(\d+?)\.(\d+))\]/) do |stamp|

next if $'.nil?

timeStamp = stamp[0]

initTime = ($2.to_i * 3600 + $3.to_i * 60 + $4.to_i) * @timeScale + $5.to_i

startCMTime = CMTimeMakeWithSeconds(initTime/@timeScale,durationTime.timescale)

next if $'.match(/\[((\d+?):(\d+?):(\d+?)\.(\d+))\]/).nil?

subtitle = $`.to_s.strip

endTimeStamp = $1

next if timeStamp == endTimeStamp

endTime = ($2.to_i * 3600 + $3.to_i * 60 + $4.to_i) * @timeScale + $5.to_i

endCMTime = CMTimeMakeWithSeconds(endTime/@timeScale,durationTime.timescale)

subtitleHash = Hash.new

subTTexts = subtitle.split(/\r?\n|\r/)

if subTTexts != []

subTTexts.map!{|x| NSMutableAttributedString.alloc.initWithString(x)}

subTTexts.map!{|x| x.addAttributes(textAttributes,range:[0,x.length]) }

subTTexts.map!{|x| x.setAlignment(NSCenterTextAlignment,range:[0,x.length])}

subTHeight = subTTexts.inject(0){|x,y| x + y.size.height + outlineWidth * 2} + (subTTexts.length - 1) * lineSpace

@maxHeight = subTHeight if subTHeight > @maxHeight

subTWidth = subTTexts.map{|x| x.size.width + outlineWidth * 2}.max

@maxWidth = subTWidth if subTWidth > @maxWidth

end

@playerLayer.addSublayer(synchLayer)

end

parentLayer.sublayers.each do |layer|

@playerLayer = layer if layer.class == NSKVONotifying_AVPlayerLayer

end

synchLayer.addSublayer(@overlayLayer)

parentLayer = @playerView.layer

#parentLayer.addSublayer(synchLayer)

prop = @playerView.frame.size.width / @videoSize.width

@overlayLayer.transform = CATransform3DMakeScale(prop,prop, 1)

subtitleHash["subtitle"] = subtitle

subtitleHash["startCMTime"] = startCMTime

subtitleHash["durationCMTime"] = CMTimeSubtract(endCMTime,startCMTime)

subTArray << subtitleHash

end

subtitleTimes = Array.new

subTImages = Array.new

subTArray.each_with_index do |subT,idx|

image = NSImage.imageWithSize([@maxWidth,@maxHeight],flipped:false,drawingHandler:Proc.new{|handler|

context = NSGraphicsContext.currentContext.graphicsPort.to_object

CGContextSetFillColorWithColor(context, NSColor.clearColor.CGColor)

CGContextFillRect(context, CGRectMake(0, 0, @maxWidth,@maxHeight))

subTs = subT['subtitle']

lineNum = subTs.length

subTs.each_with_index do |subTitle,idx2|

textPosX = (@maxWidth - subTitle.size.width)/2

line = CTLineCreateWithAttributedString(subTitle)

runs = CTLineGetGlyphRuns(line)

letters = CGPathCreateMutable()

runs.each do |run|

runFont = CTRunGetAttributes(run)['NSFont']

CTRunGetGlyphCount(run).times do |ridx|

glyphRange = CFRangeMake(ridx, 1);

glyph = Pointer.new('S',1)

position = Pointer.new(CGPoint.type,2)

CTRunGetGlyphs(run, glyphRange, glyph)

CTRunGetPositions(run, glyphRange, position)

letter = CTFontCreatePathForGlyph(runFont, glyph[0], nil)

atf = Pointer.new(CGAffineTransform.type,1)

atf[0] = CGAffineTransformMakeTranslation(position[0].x+textPosX, 8 + (fontSize+lineSpace) * (lineNum - idx2 - 1))

CGPathAddPath(letters, atf, letter)

end

end

CGContextSetTextPosition(context,textPosX, 8 + (fontSize+lineSpace) * (lineNum - idx2 - 1))

CGContextSetLineJoin(context,KCGLineJoinRound)

CGContextAddPath(context, letters)

CGContextSetStrokeColorWithColor(context,NSColor.blackColor.CGColor)

CGContextSetLineWidth(context, outlineWidth)

CGContextStrokePath(context)

CTLineDraw(line, context)

end

})

imageName = "img#{idx}"

image.setName(imageName)

bitmapRep = NSBitmapImageRep.imageRepWithData(image.TIFFRepresentation)

subTImages << bitmapRep

subtitleTimes << subT['startCMTime'].value.to_f / durationTime.value

end

synchLayer = AVSynchronizedLayer.synchronizedLayerWithPlayerItem(@playerView.player.currentItem)

synchLayer.setFrame(@playerView.frame)

synchLayer.setAutoresizingMask(KCALayerWidthSizable | KCALayerHeightSizable)

synchLayer.setMasksToBounds(true)

@overlayLayer = CALayer.layer

@overlayLayer.frame = CGRectMake((@videoSize.width - @maxWidth)/2, @offset, @maxWidth, @maxHeight)

@overlayLayer.setMasksToBounds(true)

anim = CAKeyframeAnimation.animationWithKeyPath("contents")

anim.beginTime = AVCoreAnimationBeginTimeAtZero

anim.duration = CMTimeGetSeconds(durationTime)

anim.values = subTImages.map{|x| x.CGImage}

anim.keyTimes = subtitleTimes

anim.calculationMode = KCAAnimationDiscrete

anim.removedOnCompletion = false

anim.fillMode = KCAFillModeForwards

@overlayLayer.displayIfNeeded

@overlayLayer.addAnimation(anim,forKey:"contents")

def windowDidResize(notification)

window = notification.object

if @overlayLayer

prop = @playerLayer.videoRect.size.width / @videoSize.width

widthAdjust = (@playerView.frame.size.width - @playerLayer.videoRect.size.width) / 2

heightAdjust = (@playerView.frame.size.height - @playerLayer.videoRect.size.height) / 2

@overlayLayer.transform = CATransform3DMakeScale(prop,prop, 1)

@overlayLayer.frame = CGRectMake((@playerLayer.videoRect.size.width - @maxWidth * prop)/2 + widthAdjust, @offset * prop + heightAdjust, @maxWidth * prop, @maxHeight * prop)

end

end