Search code examples
swiftavplayertvos

tvOS: Anyway to display a subtitle outside of the AVPlayer?


So the scenario is that there is a view where the user can enable/disable subtitles in an app I'm helping to develop.

On that view there is a sample text saying "This is what captions look like", and at the moment it's just a basic, unstyled UILabel. Ideally I would like it to be styled in a similar manner to how the user has customized their captions in the System Settings.

Is this possible in any way? I've envisioned two possible method:

  1. Create an AVPlayer instance and a .vtt file with the text, load it into the view and pause the player. I'm not sure this is possible with a sample video (and it would somehow have to be transparent as there is an image behind the sample sub text).

  2. Somehow get all the styling (font, size, background color, etc) the user has set for their subtitle and create an attributed string to match that

Method 2 seems like the most feasible way, but I don't know if we have access to those settings in code.


Solution

  • So I figured it out! It basically makes use a combination of the Media Accessibility API, which allows you to get the values the user has chosen for their captions/subtitle settings, Attributed Strings, and a subclass UILabel (although this could maybe be substituted with a UITextView as that will allow you to set it's UIEdgeInsets natively)

    So, first, the subclass is to allow the UILabel to be inset. This is because captions can have a background color AND a text highlight color and without the inset, the text highlight is all you see. So the function the subclass is simple:

    class InsetUILabel: UILabel {
        override func drawTextInRect(rect: CGRect) {
            let inset: CGFloat = 15
            let insets: UIEdgeInsets = UIEdgeInsets(top: inset, left: inset/2, bottom: inset, right: inset/2)
            super.drawTextInRect(UIEdgeInsetsInsetRect(rect, insets))
        }
    }
    

    And for generating the actual label. This uses a label called textSample, but you can obviously make it a little more general.

    import MediaAccessibility
    
    func styleLabel(sampleText: String) {
        let domain = MACaptionAppearanceDomain.User
    
        // Background styling
        let backgroundColor = UIColor(CGColor: MACaptionAppearanceCopyWindowColor(domain, nil).takeRetainedValue())
        let backgroundOpacity = MACaptionAppearanceGetWindowOpacity(domain, nil)
        textSample.layer.backgroundColor = backgroundColor.colorWithAlphaComponent(backgroundOpacity).CGColor
        textSample.layer.cornerRadius = MACaptionAppearanceGetWindowRoundedCornerRadius(domain, nil)
    
        // Text styling
        var textAttributes = [String:AnyObject]()
        let fontDescriptor = MACaptionAppearanceCopyFontDescriptorForStyle(domain, nil, MACaptionAppearanceFontStyle.Default).takeRetainedValue()
        let fontName = CTFontDescriptorCopyAttribute(fontDescriptor, "NSFontNameAttribute") as! String
        let fontColor = UIColor(CGColor: MACaptionAppearanceCopyForegroundColor(domain, nil).takeRetainedValue())
        let fontOpacity = MACaptionAppearanceGetForegroundOpacity(domain, nil)
        let textEdgeStyle = MACaptionAppearanceGetTextEdgeStyle(domain, nil)
        let textHighlightColor = UIColor(CGColor: MACaptionAppearanceCopyBackgroundColor(domain, nil).takeRetainedValue())
        let textHighlightOpacity = MACaptionAppearanceGetBackgroundOpacity(domain, nil)
        let textEdgeShadow = NSShadow()
        textEdgeShadow.shadowColor = UIColor.blackColor()
        let shortShadowOffset: CGFloat = 1.5
        let shadowOffset: CGFloat = 3.5
    
        switch(textEdgeStyle) {
        case .None:
            textEdgeShadow.shadowColor = UIColor.clearColor()
    
        case .DropShadow:
            textEdgeShadow.shadowOffset = CGSize(width: -shortShadowOffset, height: shortShadowOffset)
            textEdgeShadow.shadowBlurRadius = 6
    
        case .Raised:
            textEdgeShadow.shadowOffset = CGSize(width: 0, height: shadowOffset)
            textEdgeShadow.shadowBlurRadius = 5
    
        case .Depressed:
            textEdgeShadow.shadowOffset = CGSize(width: 0, height: -shadowOffset)
            textEdgeShadow.shadowBlurRadius = 5
    
        case .Uniform:
            textEdgeShadow.shadowColor = UIColor.clearColor()
            textAttributes[NSStrokeColorAttributeName] = UIColor.blackColor()
            textAttributes[NSStrokeWidthAttributeName] = -2.0
    
        default:
            break
        }
    
        textAttributes[NSFontAttributeName] = UIFont(name: fontName, size: (textSample.font?.pointSize)!)
        textAttributes[NSForegroundColorAttributeName] = fontColor.colorWithAlphaComponent(fontOpacity)
        textAttributes[NSShadowAttributeName] = textEdgeShadow
        textAttributes[NSBackgroundColorAttributeName] = textHighlightColor.colorWithAlphaComponent(textHighlightOpacity)
    
        textSample.attributedText = NSAttributedString(string: sampleText, attributes: textAttributes)
    }
    

    Now the text highlight section makes use of shadows, with values I think look pretty good, but you might want to tweak them a tiny bit. Hope this helps!