private final void init() {
    if (null != m_video) {
      MediaError status = m_video.getError();

      if (status != null) {
        setErrorCode(status.getCode());
      } else {
        if (isShowPoster()) {
          final String url = m_video.getPoster();

          if (null != url) {
            new ImageLoader(url) {
              @Override
              public void onImageElementLoad(final ImageElement elem) {
                m_postr = elem;
              }

              @Override
              public void onImageElementError(String message) {
                LienzoCore.get().error("ERROR: Getting video poster url[" + url + "] " + message);
              }
            };
          }
        }
      }
    } else {
      m_error = MessageConstants.MESSAGES.movieNotSupportedInThisBrowser();
    }
    m_inits = false;
  }
  private final void setSizes() {
    if (null != m_video) {
      final int wide = getWidth();

      final int high = getHeight();

      m_video.setWidth(wide + "px");

      m_video.setHeight(high + "px");

      m_canvas.setPixelSize(wide, high);
    }
  }
  public final Movie setPlaybackRate(double rate) {
    getAttributes().setPlaybackRate(rate);

    if (null != m_video) {
      m_video.setPlaybackRate(rate);
    }
    return this;
  }
  /**
   * Sets the movie to continuously loop or not.
   *
   * @param loop
   * @return this Movie
   */
  public Movie setLoop(boolean loop) {
    getAttributes().setLoop(loop);

    if (null != m_video) {
      m_video.setLoop(loop);
    }
    return this;
  }
  /**
   * Pauses this movie.
   *
   * @return this Movie
   */
  public Movie pause() {
    if ((null != m_video) && (false == isPaused())) {
      m_pause = true;

      m_video.pause();
    }
    return this;
  }
  /**
   * Sets the movie's volume
   *
   * @param volume
   * @return this Movie
   */
  public Movie setVolume(double volume) {
    getAttributes().setVolume(volume);

    if (null != m_video) {
      m_video.setVolume(getVolume());
    }
    return this;
  }
  private final MovieAnimation doInitialize() {
    if (null != m_video) {
      setErrorHandler(this, m_video.getVideoElement());

      String url = getURL();

      if ((null == url) || ((url = url.trim()).isEmpty()) || (url.startsWith("#"))) {
        throw new NullPointerException("null or empty or invalid url");
      }
      url = UriUtils.fromString(url).asString();

      if ((null == url) || ((url = url.trim()).isEmpty()) || (url.startsWith("#"))) {
        throw new NullPointerException("null or empty or invalid url");
      }
      m_video.setSrc(url);

      m_video.setLoop(isLoop());

      m_video.setVisible(false);

      m_video.setPlaybackRate(getPlaybackRate());

      m_video.setPreload(MediaElement.PRELOAD_AUTO);

      if (getAttributes().isDefined(Attribute.VOLUME)) {
        m_video.setVolume(getVolume());
      }
      setSizes();

      return new MovieAnimation(this, m_video);
    } else {
      return null;
    }
  }
    @Override
    public IAnimation doStart() {
      RootPanel.get().add(m_video);

      m_video.play();

      if (null == m_watch) {
        m_watch =
            m_video.addEndedHandler(
                new EndedHandler() {
                  @Override
                  public void onEnded(EndedEvent event) {
                    if (false == m_movie.isLoop()) {
                      m_movie.setEnded(true);
                    }
                  }
                });
      }
      return draw();
    }
  /**
   * Gets the height of this movie's display area
   *
   * @return int
   */
  public int getHeight() {
    if (getAttributes().isDefined(Attribute.HEIGHT)) {
      int high = (int) (getAttributes().getHeight() + 0.5);

      if (high > 0) {
        return high;
      }
    }
    if (null != m_video) {
      return m_video.getVideoHeight();
    }
    return 0;
  }
  /**
   * Gets the width of this movie's display area
   *
   * @return int
   */
  public int getWidth() {
    if (getAttributes().isDefined(Attribute.WIDTH)) {
      int wide = (int) (getAttributes().getWidth() + 0.5);

      if (wide > 0) {
        return wide;
      }
    }
    if (null != m_video) {
      return m_video.getVideoWidth();
    }
    return 0;
  }
/**
 * Movie provides a mechanism for viewing and controlling videos in a Canvas. Due to discrepancies
 * in the adoption of the Canvas specification by different vendors, you should provide multiple
 * formats of the video to ensure portability.
 */
public class Movie extends Shape<Movie> implements ImageDataFilterable<Movie> {
  private static final int MOVIE_ERROR_HIGH = 360;

  private static final int MOVIE_ERROR_WIDE = 640;

  private boolean m_inits = true;

  private boolean m_ended = true;

  private boolean m_pause = true;

  private boolean m_xorig = false;

  private String m_error = null;

  private ImageElement m_postr = null;

  private MovieEndedHandler m_onend = null;

  private final Video m_video = Video.createIfSupported();

  private final MovieAnimation m_animate;

  private final ImageDataFilterChain m_filters = new ImageDataFilterChain();

  private final ScratchPad m_canvas = new ScratchPad(0, 0);

  /**
   * Constructor. Creates an instance of a movie.
   *
   * @param url
   */
  public Movie(final String url) {
    super(ShapeType.MOVIE);

    getAttributes().setURL(url);

    m_animate = doInitialize();
  }

  public Movie(
      final String url, final ImageDataFilter<?> filter, final ImageDataFilter<?>... filters) {
    super(ShapeType.MOVIE);

    getAttributes().setURL(url);

    m_animate = doInitialize();

    setFilters(filter, filters);
  }

  protected Movie(final JSONObject node, final ValidationContext ctx) throws ValidationException {
    super(ShapeType.MOVIE, node, ctx);

    m_animate = doInitialize();
  }

  private final MovieAnimation doInitialize() {
    if (null != m_video) {
      setErrorHandler(this, m_video.getVideoElement());

      String url = getURL();

      if ((null == url) || ((url = url.trim()).isEmpty()) || (url.startsWith("#"))) {
        throw new NullPointerException("null or empty or invalid url");
      }
      url = UriUtils.fromString(url).asString();

      if ((null == url) || ((url = url.trim()).isEmpty()) || (url.startsWith("#"))) {
        throw new NullPointerException("null or empty or invalid url");
      }
      m_video.setSrc(url);

      m_video.setLoop(isLoop());

      m_video.setVisible(false);

      m_video.setPlaybackRate(getPlaybackRate());

      m_video.setPreload(MediaElement.PRELOAD_AUTO);

      if (getAttributes().isDefined(Attribute.VOLUME)) {
        m_video.setVolume(getVolume());
      }
      setSizes();

      return new MovieAnimation(this, m_video);
    } else {
      return null;
    }
  }

  private final native void setErrorHandler(Movie movie, VideoElement element)/*-{
		element.onerror = function(e) {
			[email protected]::setErrorCode(I)(e.target.error.code);
		};
    }-*/ ;

  private final String getTextBestFit(final Context2D context, final String text, final int wide) {
    double pt = LienzoCore.get().getDefaultFontSize();

    String st = LienzoCore.get().getDefaultFontStyle();

    String fm = LienzoCore.get().getDefaultFontFamily();

    String tf = Text.getFontString(pt, TextUnit.PT, st, fm);

    context.save();

    context.setToIdentityTransform();

    while (true) {
      context.setTextFont(tf);

      final TextMetrics tm = context.measureText(text);

      if (tm.getWidth() < wide) {
        break;
      }
      pt = pt - 2;

      if (pt < 6) {
        break;
      }
      tf = Text.getFontString(pt, TextUnit.PT, st, fm);
    }
    context.restore();

    return tf;
  }

  public Movie onEnded(final MovieEndedHandler onend) {
    m_onend = onend;

    return this;
  }

  @Override
  public BoundingBox getBoundingBox() {
    return new BoundingBox(0, 0, getWidth(), getHeight());
  }

  /**
   * Draws the frames of the video. If looping has been set, frames are drawn continuously in a
   * loop.
   *
   * @param context
   */
  @Override
  protected boolean prepare(final Context2D context, final Attributes attr, final double alpha) {
    if (m_inits) {
      init();

      if ((null == m_error) && (isAutoPlay())) {
        play();
      }
    }
    int wide = getWidth();

    int high = getHeight();

    if (null != m_error) {
      if (false == context.isSelection()) {
        if (wide < 1) {
          wide = MOVIE_ERROR_WIDE;
        }
        if (high < 1) {
          high = MOVIE_ERROR_HIGH;
        }
        context.save();

        context.setFillColor(ColorName.BLACK);

        context.rect(0, 0, wide, high);

        context.fill();

        context.setTextAlign(TextAlign.CENTER);

        context.setTextBaseline(TextBaseLine.MIDDLE);

        context.setTextFont(getTextBestFit(context, m_error, wide));

        context.setFillColor(ColorName.WHITE);

        context.rect(0, 0, wide, high);

        context.clip();

        context.fillText(m_error, wide / 2, high / 2);

        context.restore();
      }
    } else {
      if ((wide < 1) || (high < 1)) {
        return false;
      }
      if (context.isSelection()) {
        final String color = getColorKey();

        if (null != color) {
          context.save();

          context.setFillColor(color);

          context.fillRect(0, 0, wide, high);

          context.restore();
        }
        return false;
      }
      if (isEnded()) {
        if (null != m_postr) {
          context.save();

          context.setGlobalAlpha(alpha);

          context.drawImage(m_postr, 0, 0, wide, high);

          context.restore();
        } else {
          String fill = getFillColor();

          if (null != fill) {
            context.save();

            context.setGlobalAlpha(alpha);

            context.setFillColor(fill);

            context.fillRect(0, 0, wide, high);

            context.restore();
          }
        }
        return false;
      }
      context.save();

      context.setGlobalAlpha(alpha);

      if ((false == m_xorig) && (m_filters.isActive())) {
        try {
          m_canvas.getContext().drawImage(m_video.getElement(), 0, 0, wide, high);

          m_canvas
              .getContext()
              .putImageData(
                  m_filters.filter(m_canvas.getContext().getImageData(0, 0, wide, high), false),
                  0,
                  0);

          context.drawImage(m_canvas.getElement(), 0, 0, wide, high);
        } catch (Exception e) {
          // We should only get an exception here if the URL is cross-origin, and getImageData() is
          // basically a security exception.
          // ...or other unknown bad things, either way, turn off filtering. DSJ 7/18/2014

          context.drawImage(m_video.getElement(), 0, 0, wide, high);

          m_xorig = true;

          LienzoCore.get()
              .error("ERROR: In Movie filtering " + m_video.getSrc() + " " + e.getMessage());
        }
      } else {
        context.drawImage(m_video.getElement(), 0, 0, wide, high);
      }
      context.restore();
    }
    return false;
  }

  @Override
  public Movie setFilters(ImageDataFilter<?> filter, ImageDataFilter<?>... filters) {
    m_filters.setFilters(filter, filters);

    return this;
  }

  @Override
  public Movie addFilters(ImageDataFilter<?> filter, ImageDataFilter<?>... filters) {
    m_filters.addFilters(filter, filters);

    return this;
  }

  @Override
  public Movie removeFilters(ImageDataFilter<?> filter, ImageDataFilter<?>... filters) {
    m_filters.removeFilters(filter, filters);

    return this;
  }

  @Override
  public Movie clearFilters() {
    m_filters.clearFilters();

    return this;
  }

  @Override
  public Collection<ImageDataFilter<?>> getFilters() {
    return m_filters.getFilters();
  }

  @Override
  public Movie setFiltersActive(boolean active) {
    m_filters.setActive(active);

    return this;
  }

  @Override
  public boolean areFiltersActive() {
    return m_filters.areFiltersActive();
  }

  @Override
  public Movie setFilters(Iterable<ImageDataFilter<?>> filters) {
    m_filters.setFilters(filters);

    return this;
  }

  @Override
  public Movie addFilters(Iterable<ImageDataFilter<?>> filters) {
    m_filters.addFilters(filters);

    return this;
  }

  @Override
  public Movie removeFilters(Iterable<ImageDataFilter<?>> filters) {
    m_filters.removeFilters(filters);

    return this;
  }

  /**
   * Sets the movie's volume
   *
   * @param volume
   * @return this Movie
   */
  public Movie setVolume(double volume) {
    getAttributes().setVolume(volume);

    if (null != m_video) {
      m_video.setVolume(getVolume());
    }
    return this;
  }

  /**
   * Gets the value for the volume.
   *
   * @return double
   */
  public double getVolume() {
    return getAttributes().getVolume();
  }

  /**
   * Gets the URL for this movie.
   *
   * @return String
   */
  public String getURL() {
    return getAttributes().getURL();
  }

  /**
   * Pauses this movie.
   *
   * @return this Movie
   */
  public Movie pause() {
    if ((null != m_video) && (false == isPaused())) {
      m_pause = true;

      m_video.pause();
    }
    return this;
  }

  public boolean isPaused() {
    return m_pause;
  }

  private final void setEnded(boolean ended) {
    if (m_ended = ended) {
      if (null != m_onend) {
        final Movie movie = this;

        Scheduler.get()
            .scheduleDeferred(
                new ScheduledCommand() {
                  @Override
                  public void execute() {
                    if (null != m_onend) {
                      m_onend.onEnded(movie);
                    }
                  }
                });
      }
    }
  }

  public boolean isEnded() {
    return m_ended;
  }

  /**
   * Sets the movie to continuously loop or not.
   *
   * @param loop
   * @return this Movie
   */
  public Movie setLoop(boolean loop) {
    getAttributes().setLoop(loop);

    if (null != m_video) {
      m_video.setLoop(loop);
    }
    return this;
  }

  /**
   * Returns true if this movie is set to loop; false otherwise.
   *
   * @return boolean
   */
  public boolean isLoop() {
    return getAttributes().isLoop();
  }

  /**
   * Sets the width of this movie's display area
   *
   * @param wide
   * @return this Movie
   */
  public Movie setWidth(int wide) {
    getAttributes().setWidth(wide);

    setSizes();

    return this;
  }

  /**
   * Gets the width of this movie's display area
   *
   * @return int
   */
  public int getWidth() {
    if (getAttributes().isDefined(Attribute.WIDTH)) {
      int wide = (int) (getAttributes().getWidth() + 0.5);

      if (wide > 0) {
        return wide;
      }
    }
    if (null != m_video) {
      return m_video.getVideoWidth();
    }
    return 0;
  }

  /**
   * Sets the height of this movie's display area
   *
   * @param high
   * @return this Movie
   */
  public Movie setHeight(int high) {
    getAttributes().setHeight(high);

    setSizes();

    return this;
  }

  /**
   * Gets the height of this movie's display area
   *
   * @return int
   */
  public int getHeight() {
    if (getAttributes().isDefined(Attribute.HEIGHT)) {
      int high = (int) (getAttributes().getHeight() + 0.5);

      if (high > 0) {
        return high;
      }
    }
    if (null != m_video) {
      return m_video.getVideoHeight();
    }
    return 0;
  }

  public final Movie setPlaybackRate(double rate) {
    getAttributes().setPlaybackRate(rate);

    if (null != m_video) {
      m_video.setPlaybackRate(rate);
    }
    return this;
  }

  public final double getPlaybackRate() {
    return getAttributes().getPlaybackRate();
  }

  public final Movie setAutoPlay(boolean play) {
    getAttributes().setAutoPlay(play);

    return this;
  }

  public final boolean isAutoPlay() {
    return getAttributes().isAutoPlay();
  }

  public final Movie setShowPoster(boolean show) {
    getAttributes().setShowPoster(show);

    return this;
  }

  public final boolean isShowPoster() {
    return getAttributes().isShowPoster();
  }

  public final void play() {
    if ((null != m_video) && (null != m_animate) && (isPaused() || isEnded())) {
      m_pause = false;

      m_ended = false;

      m_animate.run();
    }
  }

  private final void setErrorCode(int code) {
    switch (code) {
      case MediaError.MEDIA_ERR_ABORTED:
        m_error = MessageConstants.MESSAGES.moviePlaybackWasAborted();
        break;
      case MediaError.MEDIA_ERR_NETWORK:
        m_error = MessageConstants.MESSAGES.movieNetworkError();
        break;
      case MediaError.MEDIA_ERR_DECODE:
        m_error = MessageConstants.MESSAGES.movieErrorInDecoding();
        break;
      case MediaError.MEDIA_ERR_SRC_NOT_SUPPORTED:
        m_error = MessageConstants.MESSAGES.movieFormatNotSupported();
        break;
    }
  }

  public final String getError() {
    return m_error;
  }

  private final void init() {
    if (null != m_video) {
      MediaError status = m_video.getError();

      if (status != null) {
        setErrorCode(status.getCode());
      } else {
        if (isShowPoster()) {
          final String url = m_video.getPoster();

          if (null != url) {
            new ImageLoader(url) {
              @Override
              public void onImageElementLoad(final ImageElement elem) {
                m_postr = elem;
              }

              @Override
              public void onImageElementError(String message) {
                LienzoCore.get().error("ERROR: Getting video poster url[" + url + "] " + message);
              }
            };
          }
        }
      }
    } else {
      m_error = MessageConstants.MESSAGES.movieNotSupportedInThisBrowser();
    }
    m_inits = false;
  }

  private final void setSizes() {
    if (null != m_video) {
      final int wide = getWidth();

      final int high = getHeight();

      m_video.setWidth(wide + "px");

      m_video.setHeight(high + "px");

      m_canvas.setPixelSize(wide, high);
    }
  }

  @Override
  public JSONObject toJSONObject() {
    JSONObject object = super.toJSONObject();

    ImageDataFilterChain chain = m_filters;

    if ((null != chain) && (chain.size() > 0)) {
      JSONArray filters = new JSONArray();

      JSONObject filter = new JSONObject();

      filter.put("active", JSONBoolean.getInstance(chain.isActive()));

      for (ImageDataFilter<?> ifilter : chain.getFilters()) {
        if (null != ifilter) {
          JSONObject make = ifilter.toJSONObject();

          if (null != make) {
            filters.set(filters.size(), make);
          }
        }
      }
      filter.put("filters", filters);

      object.put("filter", filter);
    }
    return object;
  }

  @Override
  public List<Attribute> getBoundingBoxAttributes() {
    return Arrays.asList(Attribute.WIDTH, Attribute.HEIGHT);
  }

  private static final class MovieAnimation extends IndefiniteAnimation {
    private HandlerRegistration m_watch = null;

    private final Movie m_movie;

    private final Video m_video;

    private boolean m_start = true;

    public MovieAnimation(final Movie movie, final Video video) {
      super(null);

      m_movie = movie;

      m_video = video;
    }

    @Override
    public IAnimationHandle run() {
      m_start = true;

      super.run();

      m_start = false;

      return this;
    }

    @Override
    public IAnimation doStart() {
      RootPanel.get().add(m_video);

      m_video.play();

      if (null == m_watch) {
        m_watch =
            m_video.addEndedHandler(
                new EndedHandler() {
                  @Override
                  public void onEnded(EndedEvent event) {
                    if (false == m_movie.isLoop()) {
                      m_movie.setEnded(true);
                    }
                  }
                });
      }
      return draw();
    }

    @Override
    public IAnimation doFrame() {
      return draw();
    }

    @Override
    public IAnimation doClose() {
      RootPanel.get().remove(m_video);

      if (null != m_watch) {
        m_watch.removeHandler();

        m_watch = null;
      }
      return draw();
    }

    @Override
    public boolean isRunning() {
      if (m_start) {
        return false;
      }
      if (null != m_movie.getError()) {
        return false;
      }
      if (m_movie.isEnded()) {
        return false;
      }
      if (m_movie.isPaused()) {
        return false;
      }
      return true;
    }

    private final IAnimation draw() {
      final Layer layer = m_movie.getLayer();

      if (null != layer) {
        layer.batch();
      }
      return this;
    }
  }

  public static class MovieFactory extends ShapeFactory<Movie> {
    public MovieFactory() {
      super(ShapeType.MOVIE);

      addAttribute(Attribute.URL, true);

      addAttribute(Attribute.LOOP);

      addAttribute(Attribute.WIDTH);

      addAttribute(Attribute.HEIGHT);

      addAttribute(Attribute.VOLUME);

      addAttribute(Attribute.AUTO_PLAY);

      addAttribute(Attribute.SHOW_POSTER);

      addAttribute(Attribute.PLAYBACK_RATE);
    }

    @Override
    public Movie create(JSONObject node, ValidationContext ctx) throws ValidationException {
      Movie movie = new Movie(node, ctx);

      JSONValue jval = node.get("filter");

      if (null != jval) {
        JSONObject object = jval.isObject();

        if (null != object) {
          JSONDeserializer.get().deserializeFilters(movie, object, ctx);

          jval = object.get("active");

          JSONBoolean active = jval.isBoolean();

          if (null != active) {
            movie.setFiltersActive(active.booleanValue());
          }
        }
      }
      return movie;
    }
  }
}
  /**
   * Draws the frames of the video. If looping has been set, frames are drawn continuously in a
   * loop.
   *
   * @param context
   */
  @Override
  protected boolean prepare(final Context2D context, final Attributes attr, final double alpha) {
    if (m_inits) {
      init();

      if ((null == m_error) && (isAutoPlay())) {
        play();
      }
    }
    int wide = getWidth();

    int high = getHeight();

    if (null != m_error) {
      if (false == context.isSelection()) {
        if (wide < 1) {
          wide = MOVIE_ERROR_WIDE;
        }
        if (high < 1) {
          high = MOVIE_ERROR_HIGH;
        }
        context.save();

        context.setFillColor(ColorName.BLACK);

        context.rect(0, 0, wide, high);

        context.fill();

        context.setTextAlign(TextAlign.CENTER);

        context.setTextBaseline(TextBaseLine.MIDDLE);

        context.setTextFont(getTextBestFit(context, m_error, wide));

        context.setFillColor(ColorName.WHITE);

        context.rect(0, 0, wide, high);

        context.clip();

        context.fillText(m_error, wide / 2, high / 2);

        context.restore();
      }
    } else {
      if ((wide < 1) || (high < 1)) {
        return false;
      }
      if (context.isSelection()) {
        final String color = getColorKey();

        if (null != color) {
          context.save();

          context.setFillColor(color);

          context.fillRect(0, 0, wide, high);

          context.restore();
        }
        return false;
      }
      if (isEnded()) {
        if (null != m_postr) {
          context.save();

          context.setGlobalAlpha(alpha);

          context.drawImage(m_postr, 0, 0, wide, high);

          context.restore();
        } else {
          String fill = getFillColor();

          if (null != fill) {
            context.save();

            context.setGlobalAlpha(alpha);

            context.setFillColor(fill);

            context.fillRect(0, 0, wide, high);

            context.restore();
          }
        }
        return false;
      }
      context.save();

      context.setGlobalAlpha(alpha);

      if ((false == m_xorig) && (m_filters.isActive())) {
        try {
          m_canvas.getContext().drawImage(m_video.getElement(), 0, 0, wide, high);

          m_canvas
              .getContext()
              .putImageData(
                  m_filters.filter(m_canvas.getContext().getImageData(0, 0, wide, high), false),
                  0,
                  0);

          context.drawImage(m_canvas.getElement(), 0, 0, wide, high);
        } catch (Exception e) {
          // We should only get an exception here if the URL is cross-origin, and getImageData() is
          // basically a security exception.
          // ...or other unknown bad things, either way, turn off filtering. DSJ 7/18/2014

          context.drawImage(m_video.getElement(), 0, 0, wide, high);

          m_xorig = true;

          LienzoCore.get()
              .error("ERROR: In Movie filtering " + m_video.getSrc() + " " + e.getMessage());
        }
      } else {
        context.drawImage(m_video.getElement(), 0, 0, wide, high);
      }
      context.restore();
    }
    return false;
  }