Skip to content

Commit

Permalink
feat(PanoViewer): Support video source (#48)
Browse files Browse the repository at this point in the history
Changes

  - Support video source
  - Refactoring imageLoader
  - Change API (get/setImage)
  - Update Axes Version

Ref #44
  • Loading branch information
Jongmoon Yoon authored Dec 1, 2017
1 parent 98f6af5 commit 3111655
Show file tree
Hide file tree
Showing 16 changed files with 950 additions and 150 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ egjs-view360 has the dependencies for the following libraries:

|[eg.Component](http://github.com/naver/egjs-component)|[eg.Axes](http://github.com/naver/egjs-axes)|[eg.Agent](http://github.com/naver/egjs-agent)|[es6-promise](https://github.com/stefanpenner/es6-promise)|[webvr-polyfill](https://github.com/googlevr/webvr-polyfill)|
|----|----|---|---|---|
|2.0.0+|2.1.0+|2.1.1+|4.1.1|0.9.16|
|2.0.0+|2.3.0+|2.1.1+|4.1.1|0.9.16|


## How to start developing egjs-view360?
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
},
"dependencies": {
"@egjs/agent": "^2.1.1",
"@egjs/axes": "^2.2.0",
"@egjs/axes": "^2.3.0",
"@egjs/component": "^2.0.0",
"es6-promise": "^4.1.1",
"webvr-polyfill": "^0.9.16"
Expand Down
110 changes: 66 additions & 44 deletions src/PanoImageRenderer/ImageLoader.js
Original file line number Diff line number Diff line change
@@ -1,72 +1,94 @@
const STATUS = {
"NONE": 0,
"LOADING": 1,
"LOADED": 2,
"ERROR": 3
};

export default class ImageLoader {
constructor(image) {
this.image = null;
this._image = null;
this._onceHandlers = [];
this._loadStatus = STATUS.NONE;

image && this.setImage(image);
image && this.set(image);
}

get() {
return new Promise((res, rej) => {
if (!this.image) {
rej("image is not defiend");
} else if (this.image.complete) {
res(this.image);
if (!this._image) {
rej("ImageLoader: image is not defiend");
} else if (this._loadStatus === STATUS.LOADED) {
/* Check isMaybeLoaded() first because there may have posibilities that image already loaded before get is called. for example calling get on external image onload callback.*/
res(this._image);
} else if (this._loadStatus === STATUS.LOADING) {
this._once("load", () => res(this._image));
this._once("error", () => rej("ImageLoader: failed to load images."));
} else {
ImageLoader._once(this.image, "load", () => {
res(this.image);
});
ImageLoader._once(this.image, "error", () => {
rej("failed to load images.");
});
rej("ImageLoader: failed to load images");
}
});
}

setImage(image) { // img element or img url
/**
* @param image img element or img url
*/
set(image) {
this._loadStatus = STATUS.LOADING;

if (typeof image === "string") {
this.image = new Image();
this.image.src = image;
} else if (typeof image === "object") { // img element 나 image object 이어야 함
this.image = image;
this._image = new Image();
this._image.onload = () => {
this._loadStatus = STATUS.LOADED;
};
this._image.onerror = () => {
this._loadStatus = STATUS.ERROR;
};
this._image.src = image;
} else if (typeof image === "object") {
this._image = image;
}

// promise for image
return this.get();
if (ImageLoader._isMaybeLoaded(this._image)) {
// Already loaded image
this._loadStatus = STATUS.LOADED;
}
}

static _once(target, type, listener) {
target.addEventListener(type, function fn(event) {
target.removeEventListener(type, fn);
listener(event);
});
// target.addEventListener(type, listener);
static _isMaybeLoaded(image) {
return image && image.naturalWidth !== 0;
}

isImageLoaded() {
if (!this.image) {
return false;
}
_once(type, listener) {
const target = this._image;

return !!this.image.src && !!this.image.complete;
}
const fn = event => {
target.removeEventListener(type, fn);
listener(event);
};

/**
* TODO: 이 기능이 정말로 필요한가?
*/
cancelLoadImage() {
if (!!this.image && !this.isImageLoaded()) {
this.image.src = "";
}
target.addEventListener(type, fn);
this._onceHandlers.push({type, fn});
}

loadImage() {
if (this._imageURL && !this.isImageLoaded()) {
this.image.setAttribute("crossorigin", "anonymous");
this.image.src = this._imageURL;
}
getStatus() {
return this._loadStatus;
}

destroy() {
this.image = null;
this._onceHandlers.forEach(handler => {
this._image.removeEventListener(handler.type, handler.fn);
});
this._onceHandlers = [];
/**
* Init event handlers
*/
this._image.onload = null;
this._image.onerror = null;
this._image.src = "";
this._image = null;
this._loadStatus = STATUS.NONE;
}
}

ImageLoader.STATUS = STATUS;
70 changes: 51 additions & 19 deletions src/PanoImageRenderer/PanoImageRenderer.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Component from "@egjs/component";
import ImageLoader from "./ImageLoader";
import VideoLoader from "./VideoLoader";
import WebGLUtils from "./WebGLUtils";
import CubeRenderer from "./renderer/CubeRenderer";
import SphereRenderer from "./renderer/SphereRenderer";
Expand Down Expand Up @@ -38,7 +39,7 @@ const ERROR_TYPE = {
};

export default class PanoImageRenderer extends Component {
constructor(image, width, height, sphericalConfig) {
constructor(image, width, height, isVideo, sphericalConfig) {
// Super constructor
super();

Expand All @@ -65,21 +66,43 @@ export default class PanoImageRenderer extends Component {
this.canvas = this._initCanvas(width, height);

this._image = null;
this._imageLoader = new ImageLoader();

this._onImageLoad = this._onImageLoad.bind(this);
this._onImageError = this._onImageError.bind(this);
this._onContentLoad = this._onContentLoad.bind(this);
this._onContentError = this._onContentError.bind(this);

this.setImage({image, imageType: sphericalConfig.imageType});
if (image) {
this.setImage({image, imageType: sphericalConfig.imageType, isVideo});
}
}

getContent() {
if (!this.isImageLoaded()) {
return null;
}

return this._image;
}

setImage({image, imageType}) {
setImage({image, imageType, isVideo = false}) {
this._isVideo = isVideo;
this._setImageType(imageType);

if (this._contentLoader) {
this._contentLoader.destroy();
}

if (isVideo) {
this._contentLoader = new VideoLoader();
} else {
this._contentLoader = new ImageLoader();
}

// img element or img url
return this._imageLoader.setImage(image)
.then(this._onImageLoad)
.catch(this._onImageError);
this._contentLoader.set(image);

return this._contentLoader.get()
.then(this._onContentLoad)
.catch(this._onContentError);
}

_setImageType(imageType) {
Expand Down Expand Up @@ -120,7 +143,7 @@ export default class PanoImageRenderer extends Component {
return canvas;
}

_onImageError(error) {
_onContentError(error) {
this._image = null;

this.trigger(EVENTS.ERROR, {
Expand All @@ -131,12 +154,17 @@ export default class PanoImageRenderer extends Component {
return false;
}

_onImageLoad(image) {
_onContentLoad(image) {
// 이미지의 사이즈를 캐시한다.
// image is reference for content in contentLoader, so it may be not valid if contentLoader is destroyed.
this._image = image;

// 이벤트 발생. 여기에 핸들러로 render 하는 걸 넣어준다.
this.trigger(EVENTS.IMAGE_LOADED);
this.trigger(EVENTS.IMAGE_LOADED, {
content: this._image,
isVideo: this._isVideo,
projectionType: this._imageType
});
return true;
}

Expand All @@ -145,18 +173,18 @@ export default class PanoImageRenderer extends Component {
}

cancelLoadImage() {
this._imageLoader.cancelLoadImage();
this._contentLoader.destroy();
}

bindTexture() {
return new Promise((res, rej) => {
if (!this._imageLoader) {
if (!this._contentLoader) {
rej("ImageLoader is not initialized");
return;
}

this._imageLoader.get()
.then(() => this._bindTexture(), () => rej("ImageLoader has failed to get image."))
this._contentLoader.get()
.then(() => this._bindTexture(), rej)
.then(res);
});
}
Expand Down Expand Up @@ -190,7 +218,7 @@ export default class PanoImageRenderer extends Component {
}

destroy() {
this._imageLoader.destroy();
this._contentLoader.destroy();

this.detach();
this.forceContextLoss();
Expand Down Expand Up @@ -378,7 +406,7 @@ export default class PanoImageRenderer extends Component {
}

renderWithQuaternion(quaternion, fieldOfView) {
if (!this.hasRenderingContext()) {
if (!this.isImageLoaded() || !this.hasRenderingContext()) {
return;
}

Expand Down Expand Up @@ -418,10 +446,14 @@ export default class PanoImageRenderer extends Component {
}

render(yaw, pitch, fieldOfView) {
if (!this.hasRenderingContext()) {
if (!this.isImageLoaded() || !this.hasRenderingContext()) {
return;
}

if (this._isVideo) { /* TODO: && Check if isPlaying */
this._bindTexture();
}

if (this._lastYaw !== null && this._lastYaw === yaw &&
this._lastPitch !== null && this._lastPitch === pitch &&
this.fieldOfView && this.fieldOfView === fieldOfView &&
Expand Down
72 changes: 72 additions & 0 deletions src/PanoImageRenderer/VideoLoader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/* Ref https://www.w3schools.com/tags/av_prop_readystate.asp */
const READY_STATUS = {
HAVE_NOTHING: 0, // no information whether or not the audio/video is ready
HAVE_METADATA: 1, // HAVE_METADATA - metadata for the audio/video is ready
HAVE_CURRENT_DATA: 2, // data for the current playback position is available, but not enough data to play next frame/millisecond
HAVE_FUTURE_DATA: 3, // data for the current and at least the next frame is available
HAVE_ENOUGH_DATA: 4 // enough data available to start playing
};

export default class VideoLoader {
constructor(video) {
this._handlers = [];
video && this.set(video);
}

set(video) {
if (typeof video === "string") {
// url
this._video = document.createElement("video");
this._video.src = video;
} else if (video instanceof HTMLVideoElement) {
// video tag
this._video = video;
} else {
this.destroy();
}
}

get() {
/**
* TODO: How about to resolve(null) if video is defiend.
*/
return new Promise((res, rej) => {
if (!this._video) {
rej("VideoLoader: video is undefined");
} else if (this._video.readyState === READY_STATUS.HAVE_ENOUGH_DATA) {
res(this._video);
} else {
this._once("canplaythrough", () => {
res(this._video);
});
this._once("error", () => rej(`VideoLoader: failed to load ${this._video.src}`));
this._video.load();
}
});
}

destroy() {
this._handlers.forEach(handler => {
this._video.removeEventListener(handler.type, handler.fn);
});
this._handlers = [];

if (this._video) {
this._video.pause();
this._video.src = "";
this._video = null;
}
}

_once(type, listener) {
const target = this._video;

const fn = event => {
target.removeEventListener(type, fn);
listener(event);
};

target.addEventListener(type, fn);
this._handlers.push({type, fn});
}
}
4 changes: 2 additions & 2 deletions src/PanoImageRenderer/renderer/SphereRenderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export default class SphereRenderer extends Renderer {
}

const maxTextureSize = gl.getParameter(gl.MAX_TEXTURE_SIZE);
const width = image.width;// imageWidth;
const height = image.height;// imageHeight;
const width = image.naturalWidth || image.videoWidth;// imageWidth;
const height = image.naturalHeight || image.videoHeight;// imageHeight;
const aspectRatio = height / width;
const canvas = document.createElement("canvas");

Expand Down
Loading

0 comments on commit 3111655

Please sign in to comment.