Skip to content

Instantly share code, notes, and snippets.

@esidegallery
Last active June 8, 2022 13:06
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save esidegallery/f1326e4a26206bf8da9a4bcab11bd0f8 to your computer and use it in GitHub Desktop.
Save esidegallery/f1326e4a26206bf8da9a4bcab11bd0f8 to your computer and use it in GitHub Desktop.
Feathers (Starling) ImageLoader designed to display a video texture whose coded height is different from its display height.
package feathers.controls
{
import feathers.layout.HorizontalAlign;
import feathers.layout.VerticalAlign;
import flash.display.BitmapData;
import flash.display3D.textures.VideoTexture;
import flash.geom.Point;
import flash.geom.Rectangle;
import starling.core.Starling;
import starling.display.Image;
import starling.display.Quad;
import starling.display.Stage;
import starling.rendering.Painter;
import starling.textures.RenderTexture;
import starling.textures.Texture;
import starling.utils.Color;
import starling.utils.Pool;
import starling.utils.RectangleUtil;
/**
* ImageLoader designed for displaying VideoPlayer textures.
* Supports setting of video display dimensions and coded height for aspect correction,
* plus the ability to freeze frame (via a RenderTexture) to facilitate seamless looping.
*/
public class VideoTextureImageLoader extends ImageLoader
{
private static const INVALIDATION_FLAG_VIDEO_SOURCE:String = "videoSource";
private static const HELPER_RECTANGLE:Rectangle = new Rectangle;
private static const HELPER_RECTANGLE2:Rectangle = new Rectangle;
private var _videoSource:Texture;
override public function get source():Object
{
if (_videoSource != null)
{
return _videoSource;
}
validate();
return super.source;
}
override public function set source(value:Object):void
{
if (_renderTexture != null)
{
// If renderTexture exists, then it takes precedence over videoSource:
disposeRenderTexture();
}
else if (_videoSource == value)
{
return;
}
if (value != null && value is Texture && (value as Texture).base is VideoTexture)
{
_videoSource = value as Texture;
}
else
{
super.source = value;
}
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
}
private var _videoDisplayWidth:int;
public function get videoDisplayWidth():int
{
return _videoDisplayWidth;
}
public function set videoDisplayWidth(value:int):void
{
if (_videoDisplayWidth == value)
{
return;
}
_videoDisplayWidth = value;
if (_videoSource == null)
{
return;
}
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
}
private var _videoDisplayHeight:int;
public function get videoDisplayHeight():int
{
return _videoDisplayHeight;
}
public function set videoDisplayHeight(value:int):void
{
if (_videoDisplayHeight == value)
{
return;
}
_videoDisplayHeight = value;
if (_videoSource == null)
{
return;
}
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
}
private var _videoCodedHeight:int;
public function get videoCodedHeight():int
{
return _videoCodedHeight;
}
public function set videoCodedHeight(value:int):void
{
if (_videoCodedHeight == value)
{
return;
}
_videoCodedHeight = value;
if (_videoSource == null)
{
return;
}
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
}
protected var _renderTexture:RenderTexture;
protected var _textureScaleMultiplierX:Number = 1;
protected var _textureScaleMultiplierY:Number = 1;
/**
* To be called on VideoPlayer's clear event, to ensure the no-longer-valid
* video texture will no longer be displayed. Will not clear the RenderTexture if being shown.
*/
public function clear():void
{
if (_videoSource == null)
{
return;
}
_videoSource = null;
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
validate();
}
/**
* Draws the current frame to a RenderTexture,
* which will be automatically disposed of.
*/
public function freezeFrame():void
{
disposeRenderTexture();
if (_videoSource == null || _videoSource.width == 0 || _videoSource.height == 0)
{
return;
}
var image:Image = new Image(_videoSource);
_renderTexture = new RenderTexture(image.width, image.height);
_renderTexture.draw(image);
image.dispose();
invalidate(INVALIDATION_FLAG_VIDEO_SOURCE);
validate();
}
override protected function draw():void
{
var videoSourceInvalid:Boolean = isInvalid(INVALIDATION_FLAG_VIDEO_SOURCE);
if (videoSourceInvalid)
{
commitVideoSource();
}
super.draw();
}
protected function commitVideoSource():void
{
_textureScaleMultiplierX = 1;
_textureScaleMultiplierY = 1;
var newSource:Texture = _renderTexture || _videoSource;
if (newSource == null)
{
return;
}
if (_videoDisplayHeight > 0 && _videoCodedHeight > 0 && _videoDisplayHeight != _videoCodedHeight)
{
var cropRect:Rectangle = Pool.getRectangle(0, 0, newSource.width, newSource.height - (_videoCodedHeight - _videoDisplayHeight));
newSource = Texture.fromTexture(newSource, cropRect);
Pool.putRectangle(cropRect);
}
if (_videoDisplayWidth > 0)
{
_textureScaleMultiplierX = _videoDisplayWidth / newSource.width;
}
if (_videoDisplayHeight > 0)
{
_textureScaleMultiplierY = _videoDisplayHeight / newSource.height;
}
setInvalidationFlag(INVALIDATION_FLAG_DATA);
// Note that newSource may be a cropped subtexture of videoSource or renderTexture,
// not necessarily the textures themselves:
super.source = newSource;
}
override protected function autoSizeIfNeeded():Boolean
{
var needsWidth:Boolean = _explicitWidth !== _explicitWidth; // isNaN
var needsHeight:Boolean = _explicitHeight !== _explicitHeight; // isNaN
var needsMinWidth:Boolean = _explicitMinWidth !== _explicitMinWidth; // isNaN
var needsMinHeight:Boolean = _explicitMinHeight !== _explicitMinHeight; // isNaN
if (!needsWidth && !needsHeight && !needsMinWidth && !needsMinHeight)
{
return false;
}
var heightScale:Number = 1;
var widthScale:Number = 1;
var textureScaleX:Number = textureScale * _textureScaleMultiplierX;
var textureScaleY:Number = textureScale * _textureScaleMultiplierY;
if (scaleContent && maintainAspectRatio &&
scaleMode !== starling.utils.ScaleMode.NONE &&
scale9Grid === null)
{
if (!needsHeight)
{
heightScale = _explicitHeight / (_currentTextureHeight * textureScaleY);
}
else if (_explicitMaxHeight < _currentTextureHeight)
{
heightScale = _explicitMaxHeight / (_currentTextureHeight * textureScaleY);
}
else if (_explicitMinHeight > _currentTextureHeight)
{
heightScale = _explicitMinHeight / (_currentTextureHeight * textureScaleY);
}
if (!needsWidth)
{
widthScale = _explicitWidth / (_currentTextureWidth * textureScaleX);
}
else if (_explicitMaxWidth < _currentTextureWidth)
{
widthScale = _explicitMaxWidth / (_currentTextureWidth * textureScaleX);
}
else if (_explicitMinWidth > _currentTextureWidth)
{
widthScale = _explicitMinWidth / (_currentTextureWidth * textureScaleX);
}
}
var newWidth:Number = _explicitWidth;
if (needsWidth)
{
if (_currentTextureWidth === _currentTextureWidth) // !isNaN
{
newWidth = _currentTextureWidth * textureScaleX * heightScale;
}
else
{
newWidth = 0;
}
newWidth += _paddingLeft + _paddingRight;
}
var newHeight:Number = _explicitHeight;
if (needsHeight)
{
if (_currentTextureHeight === _currentTextureHeight) // !isNaN
{
newHeight = _currentTextureHeight * textureScaleY * widthScale;
}
else
{
newHeight = 0;
}
newHeight += _paddingTop + _paddingBottom;
}
// This ensures that an ImageLoader can recover from width or height
// being set to 0 by percentWidth or percentHeight
if (needsHeight && needsMinHeight)
{
// If no height values are set, use the original texture width
// for the minWidth
heightScale = 1;
}
if (needsWidth && needsMinWidth)
{
// If no width values are set, use the original texture height
// for the minHeight
widthScale = 1;
}
var newMinWidth:Number = _explicitMinWidth;
if (needsMinWidth)
{
if (_currentTextureWidth === _currentTextureWidth) // !isNaN
{
newMinWidth = _currentTextureWidth * textureScaleX * heightScale;
}
else
{
newMinWidth = 0;
}
newMinWidth += _paddingLeft + _paddingRight;
}
var newMinHeight:Number = _explicitMinHeight;
if (needsMinHeight)
{
if (_currentTextureHeight === _currentTextureHeight) // !isNaN
{
newMinHeight = _currentTextureHeight * textureScaleY * widthScale;
}
else
{
newMinHeight = 0;
}
newMinHeight += _paddingTop + _paddingBottom;
}
return saveMeasurements(newWidth, newHeight, newMinWidth, newMinHeight);
}
override protected function layout():void
{
if (!image || !_currentTexture)
{
return;
}
if (scaleContent)
{
if (maintainAspectRatio && scale9Grid === null)
{
HELPER_RECTANGLE.x = 0;
HELPER_RECTANGLE.y = 0;
HELPER_RECTANGLE.width = _currentTextureWidth * textureScale * _textureScaleMultiplierX;
HELPER_RECTANGLE.height = _currentTextureHeight * textureScale * _textureScaleMultiplierY;
HELPER_RECTANGLE2.x = 0;
HELPER_RECTANGLE2.y = 0;
HELPER_RECTANGLE2.width = actualWidth - _paddingLeft - _paddingRight;
HELPER_RECTANGLE2.height = actualHeight - _paddingTop - _paddingBottom;
RectangleUtil.fit(HELPER_RECTANGLE, HELPER_RECTANGLE2, scaleMode, false, HELPER_RECTANGLE);
image.x = HELPER_RECTANGLE.x + _paddingLeft;
image.y = HELPER_RECTANGLE.y + _paddingTop;
image.width = HELPER_RECTANGLE.width;
image.height = HELPER_RECTANGLE.height;
}
else
{
image.x = _paddingLeft;
image.y = _paddingTop;
image.width = actualWidth - _paddingLeft - _paddingRight;
image.height = actualHeight - _paddingTop - _paddingBottom;
}
}
else
{
var imageWidth:Number = _currentTextureWidth * textureScale * _textureScaleMultiplierX;
var imageHeight:Number = _currentTextureHeight * textureScale * _textureScaleMultiplierY;
if (_horizontalAlign === HorizontalAlign.RIGHT)
{
image.x = actualWidth - _paddingRight - imageWidth;
}
else if (_horizontalAlign === HorizontalAlign.CENTER)
{
image.x = _paddingLeft + ((actualWidth - _paddingLeft - _paddingRight) - imageWidth) / 2;
}
else // left
{
image.x = _paddingLeft;
}
if (_verticalAlign === VerticalAlign.BOTTOM)
{
image.y = actualHeight - _paddingBottom - imageHeight;
}
else if (_verticalAlign === VerticalAlign.MIDDLE)
{
image.y = _paddingTop + ((actualHeight - _paddingTop - _paddingBottom) - imageHeight) / 2;
}
else // top
{
image.y = _paddingTop;
}
image.width = imageWidth;
image.height = imageHeight;
}
if ((!scaleContent || (maintainAspectRatio && scaleMode !== starling.utils.ScaleMode.SHOW_ALL)) &&
(actualWidth != imageWidth || actualHeight != imageHeight))
{
var mask:Quad = image.mask as Quad;
if (mask !== null)
{
mask.x = 0;
mask.y = 0;
mask.width = actualWidth;
mask.height = actualHeight;
}
else
{
mask = new Quad(1, 1, 0xff00ff);
// The initial dimensions cannot be 0 or there's a runtime error,
// and these values might be 0
mask.width = actualWidth;
mask.height = actualHeight;
image.mask = mask;
addChild(mask);
}
}
else
{
mask = image.mask as Quad;
if (mask !== null)
{
mask.removeFromParent(true);
image.mask = null;
}
}
}
override public function drawToBitmapData(out:BitmapData = null, color:uint = 0, alpha:Number = 0.0):BitmapData
{
var painter:Painter = Starling.painter;
var stage:Stage = Starling.current.stage;
var viewPort:Rectangle = Starling.current.viewPort;
var stageWidth:Number = stage.stageWidth;
var stageHeight:Number = stage.stageHeight;
var scaleX:Number = viewPort.width / stageWidth;
var scaleY:Number = viewPort.height / stageHeight;
var backBufferScale:Number = painter.backBufferScaleFactor;
var totalScaleX:Number = scaleX * backBufferScale;
var totalScaleY:Number = scaleY * backBufferScale;
var projectionX:Number, projectionY:Number;
var bounds:Rectangle;
if (this is Stage)
{
projectionX = viewPort.x < 0 ? -viewPort.x / scaleX : 0.0;
projectionY = viewPort.y < 0 ? -viewPort.y / scaleY : 0.0;
out ||= new BitmapData(painter.backBufferWidth * backBufferScale,
painter.backBufferHeight * backBufferScale);
}
else
{
bounds = getBounds(parent, HELPER_RECTANGLE);
projectionX = bounds.x;
projectionY = bounds.y;
out ||= new BitmapData(Math.ceil(bounds.width * totalScaleX),
Math.ceil(bounds.height * totalScaleY));
}
color = Color.multiply(color, alpha); // premultiply alpha
painter.pushState();
painter.setupContextDefaults();
painter.state.renderTarget = null;
painter.state.setModelviewMatricesToIdentity();
painter.setStateTo(transformationMatrix);
// Images that are bigger than the current back buffer are drawn in multiple steps.
var stepX:Number;
var stepY:Number = projectionY;
var stepWidth:Number = painter.backBufferWidth / scaleX;
var stepHeight:Number = painter.backBufferHeight / scaleY;
var positionInBitmap:Point = Pool.getPoint(0, 0);
var boundsInBuffer:Rectangle = Pool.getRectangle(0, 0,
painter.backBufferWidth * backBufferScale,
painter.backBufferHeight * backBufferScale);
while (positionInBitmap.y < out.height)
{
stepX = projectionX;
positionInBitmap.x = 0;
while (positionInBitmap.x < out.width)
{
painter.clear(color, alpha);
painter.state.setProjectionMatrix(stepX, stepY, stepWidth, stepHeight,
stageWidth, stageHeight, stage.cameraPosition);
if (mask)
painter.drawMask(mask, this);
if (filter)
filter.render(painter);
else
render(painter);
if (mask)
painter.eraseMask(mask, this);
painter.finishMeshBatch();
// For some reason the bitmapdata is distorted depending the size of the stageHeight and stageWidth on windows. Throwing in an additional bitmapdata and using copyPixels method fixes it.
var bmd:BitmapData = new BitmapData(stepWidth, stepHeight, true, 0x00ffffff);
painter.context.drawToBitmapData(bmd, boundsInBuffer);
out.copyPixels(bmd, boundsInBuffer, positionInBitmap);
stepX += stepWidth;
positionInBitmap.x += stepWidth * totalScaleX;
}
stepY += stepHeight;
positionInBitmap.y += stepHeight * totalScaleY;
}
painter.popState();
Pool.putRectangle(boundsInBuffer);
Pool.putPoint(positionInBitmap);
return out;
}
protected function disposeRenderTexture():void
{
if (_renderTexture == null)
{
return;
}
_renderTexture.dispose();
_renderTexture = null;
}
override public function dispose():void
{
disposeRenderTexture();
super.dispose();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment