Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support video source #48

Merged
merged 26 commits into from
Dec 1, 2017
Merged

Support video source #48

merged 26 commits into from
Dec 1, 2017

Conversation

jongmoon
Copy link

Issue

#44

Details

We need to discuss video interface on this branch (video#44)

@coveralls
Copy link

Coverage Status

Coverage increased (+13.9%) to 51.374% when pulling d71b053 on jongmoon:video#44 into 98f6af5 on naver:video#44.

@@ -38,7 +39,7 @@ const ERROR_TYPE = {
};

export default class PanoImageRenderer extends Component {
constructor(image, width, height, sphericalConfig) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isVideo is used to distinguish image is image or video.

Other consideration

  1. image => content
    • Issue: It's difficult to distinguish image from image if content is string.
  2. Add isVideo property on sphericalConfig
    • Issue: isVideo is not related with sphericalConfig

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood.


this._onImageLoad = this._onImageLoad.bind(this);
this._onImageError = this._onImageError.bind(this);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've replaced image with content.

media was a candidate.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the media element only contains video and audio, the name media is not appropriate for image and video.

I'm okay with unified loader name as content loader.

}

setImage({image, imageType}) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't separate a function into video and image. because their logic are almost same.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

this._contentLoader = new VideoLoader();
} else {
this._contentLoader = new ImageLoader();
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

contentLoader uses concept of inheritance. and use get/set common interface.

I've tried to combine imageLoader and videoLoader. But it is somewhat different logic. If being combined, code will be difficult to read.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this.

@@ -145,19 +156,20 @@ export default class PanoImageRenderer extends Component {
}

cancelLoadImage() {
this._imageLoader.cancelLoadImage();
this._contentLoader.destroy();
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed cancelImage. and replaced it destroy.

this._contentLoader.get()
.then(() => this._bindTexture())
.then(res)
.catch(rej);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use catch to check res fails.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line with catch cannot resolve failing situation.

inst.bindTexture().then(() => {}).catch(() => {});

@@ -32,7 +33,7 @@ export default class PanoViewer extends Component {
* @param {Array} [config.fovRange=[30, 110]] Range of controllable vertical field of view values <ko>제어 가능한 수직 field of view 값의 범위</ko>
* @param {Function} [config.checkSupport] A function that returns a boolean value that determines whether the component is working. <ko>뷰어가 작동할 지 여부를 결정하는 부울 값을 반환하는 함수입니다.</ko>
*/
constructor(container, options) {
constructor(container, options = {}) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Script error will be prevented if options are omitted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good

@@ -57,8 +58,19 @@ export default class PanoViewer extends Component {
return this;
}

if (!!options.image === !!options.video) {
Copy link
Author

@jongmoon jongmoon Nov 16, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If image and video are both omitted or both specified, then trigger INVALID_RESOURCE error. In other words, only one resource (image or video) can be specified.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great.

There should be documentation for the new event.
like below

/**
* Events that is fired when animation which is triggered by inertia is ended.
* @ko 관성에 의한 애니메이션 동작이 완료되었을때 발생하는 이벤트
* @name eg.view360.PanoViewer#animationEnd
* @event
* @example
*
* viwer.on({
*	"animationEnd" : function(evt) {
*		// animation is ended.
* });
*/

this jsdoc is written inside of _triggerEvent method.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I will apply it on PanoViewer#error

return {
video: this._image,
type: this._imageType
};
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Be careful its return type is differ from getImage();

getVideo returns,

  • video:
  • type: projectionType (equirectangular or cubemap)

getImage returns

  • image: Image Object
  • imageType: projectionType (equirectangular or cubemap)

Both return projection type but it expose other interface(type/imageType) each other.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj As you mentioned in offline, I think it would be better not to expose type information.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found imageType accessing code that use like below.

panoViewer.setImage({
  image: image,
  imageType: panoViewer.getImage().imageType
});

We need to decide name of imageType getter.

  1. projectionType
  2. imageType
  3. type
  4. and others?

}

// promise for image
return this.get();
}

static _isMaybeLoaded(image) {
return image && (image.width || image.height);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

width and height property in image object is not read-only.
However, naturalWidth is read-only, so it is good to check that the image has been loaded.

The complete property is not appropriate because image objects that do not have src will also have true values.

I suggest that you apply the following statement.

return image && (image.naturalWidth !== 0);

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj good information, thank you!


this._onImageLoad = this._onImageLoad.bind(this);
this._onImageError = this._onImageError.bind(this);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because the media element only contains video and audio, the name media is not appropriate for image and video.

I'm okay with unified loader name as content loader.

this._contentLoader = new VideoLoader();
} else {
this._contentLoader = new ImageLoader();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree with this.

}

setImage({image, imageType}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK

@@ -378,7 +390,7 @@ export default class PanoImageRenderer extends Component {
}

renderWithQuaternion(quaternion, fieldOfView) {
if (!this.hasRenderingContext()) {
if (!this.isImageLoaded() || !this.hasRenderingContext()) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The isImageLoaded method seems like no longer belongs to the PanoImageRenderer.

How about removing isImageLoaded method from PanoImageRenderer and checking loading status with method on VideoLoader and ImageLoader?

like

this._imageLoader.isContentReady();
this._videoLoader.isContentReady();

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj I got it!

@@ -32,7 +33,7 @@ export default class PanoViewer extends Component {
* @param {Array} [config.fovRange=[30, 110]] Range of controllable vertical field of view values <ko>제어 가능한 수직 field of view 값의 범위</ko>
* @param {Function} [config.checkSupport] A function that returns a boolean value that determines whether the component is working. <ko>뷰어가 작동할 지 여부를 결정하는 부울 값을 반환하는 함수입니다.</ko>
*/
constructor(container, options) {
constructor(container, options = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good

@@ -57,8 +58,19 @@ export default class PanoViewer extends Component {
return this;
}

if (!!options.image === !!options.video) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great.

There should be documentation for the new event.
like below

/**
* Events that is fired when animation which is triggered by inertia is ended.
* @ko 관성에 의한 애니메이션 동작이 완료되었을때 발생하는 이벤트
* @name eg.view360.PanoViewer#animationEnd
* @event
* @example
*
* viwer.on({
*	"animationEnd" : function(evt) {
*		// animation is ended.
* });
*/

this jsdoc is written inside of _triggerEvent method.

@coveralls
Copy link

Coverage Status

Coverage increased (+16.9%) to 54.342% when pulling ba83d18 on jongmoon:video#44 into 98f6af5 on naver:video#44.

export default class VideoLoader {
constructor(video) {
this._isLoaded = false;
this._handlers = [];
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj I added event handler for managing content load event. Because if not managed, it fires event although it has been destroyed.

@@ -57,8 +58,19 @@ export default class PanoViewer extends Component {
return this;
}

if (!!options.image && !!options.video) {
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj I had changed above condition if (!!options.image === !!options.video). but I changed it . because of allowing no content specified.

}

return this._photoSphereRenderer.getContent();
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj getVideo returns HTMLVideoElement

@coveralls
Copy link

Coverage Status

Coverage increased (+16.9%) to 54.301% when pulling 80bd42c on jongmoon:video#44 into 98f6af5 on naver:video#44.

* Init event handlers
*/
this._image.onload = null;
this._image.onerror = null;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj Initialize event handler for preventing event from being fired after destroy

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to remove listeners which attached with addEventListener, just like in VideoLoader.js

content: this._image,
isVideo: this._isVideo,
projectionType: this._imageType
});
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj Renderer triggers IMAGE_LOADED event with param {content, isVideo, projectionType}

this._imageType = options.imageType || PanoImageRenderer.ImageType.EQUIRECTANGULAR;
this._image = options.image || options.video;
this._isVideo = !!options.video;
this._projectionType = options.projectionType || PanoImageRenderer.ImageType.EQUIRECTANGULAR;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj _imageType -> _projectionType on PanoViewer. It is not applied on PanoImageRenderer

if (this._image && isVideo !== this._isVideo) {
console.warn("Not support changing content type(Image <--> Video)");
return this;
}
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj I'm begging your opinion for this.

In above code, if content type is differed then it will do nothing. I consider following way if set fails.

  1. throw
  2. return false (if fails)
  3. console
  4. ignore

But each have each problem.

  1. User should try/catch
  2. Different set interface (others returns this)
  3. User may not recognize it.
  4. User cannot judge what is the problem.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.warn would be nice.
And it will be better to show this is temporary state with warning message.

"Currently not supporting changing content type(Image <--> Video)"

}
);
this._bindRendererHandler();
}

_bindRendererHandler() {
this._photoSphereRenderer.on(PanoImageRenderer.EVENTS.IMAGE_LOADED, e => {
this.trigger(EVENTS.CONTENT_LOADED, e);
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj I added "contentLoaded" event.

this._contentLoader.get()
.then(() => this._bindTexture())
.then(res)
.catch(rej);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The line with catch cannot resolve failing situation.

inst.bindTexture().then(() => {}).catch(() => {});

res(this._image);
} else if (this._loadStatus === STATUS.LOADING) {
ImageLoader._once(this._image, "load", () => {
this._loadStatus = STATUS.LOADED;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to set state with getter method.

ImageLoader._once(this.image, "error", () => {
rej("failed to load images.");
ImageLoader._once(this._image, "error", () => {
this._loadStatus = STATUS.ERROR;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to set state with getter method.

* Init event handlers
*/
this._image.onload = null;
this._image.onerror = null;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

need to remove listeners which attached with addEventListener, just like in VideoLoader.js

res(this._video);
} else {
this._once("canplaythrough", () => {
this._isLoaded = false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to set loading status in setter method.

}

set(video) {
if (typeof video === "string") {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting loading status is missing. like

this._once("canplaythrough", () => {
    this._isLoaded = true;
});

and should check the case that the video element already triggered canplaythrough event.
in that case canplaythrough event will not triggered.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@happyhj So I check it video status like below on getter.

else if (this._video.readyState === READY_STATUS.HAVE_ENOUGH_DATA) {
	res(this._video);
}

const width = image.width;// imageWidth;
const height = image.height;// imageHeight;
const width = image.width || image.videoWidth;// imageWidth;
const height = image.height || image.videoHeight;// imageHeight;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is safer to use read-only property like videoWidth of video element.
How about using naturalWidth and naturalHeight property for image element?

if (this._image && isVideo !== this._isVideo) {
console.warn("Not support changing content type(Image <--> Video)");
return this;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

console.warn would be nice.
And it will be better to show this is temporary state with warning message.

"Currently not supporting changing content type(Image <--> Video)"

Jongmoon Yoon added 6 commits December 1, 2017 11:23
  - refactoring ImageLoader
use readyState to check if loaded

Ref naver#44
add getter for loadStatus

Ref naver#44
replace width/height with naturalWidth/naturalHeight to use origingal size

Ref naver#44
@coveralls
Copy link

Coverage Status

Coverage increased (+16.8%) to 54.206% when pulling b7930d1 on jongmoon:video#44 into 98f6af5 on naver:video#44.

Copy link
Contributor

@happyhj happyhj left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!
And I also reviewed test code.

this.inst = new ImageLoader("https://s.pstatic.net/static/www/mobile/edit/2015/0318/mobile_110629401121.png");
it("should get image URL as a parameter", function(done) {
// this.inst = new ImageLoader("https://s.pstatic.net/static/www/mobile/edit/2015/0318/mobile_110629401121.png");
this.inst = new ImageLoader("./images/PanoViewer/waterpark_preview.jpg");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is better to use naturalWidth/Height property rather than width/height for checking image is fully loaded.

assert.isOk(image);
assert.isOk(image.complete);
assert.isNumber(image.naturalWidth);
assert.isNumber(image.naturalHeight);
assert.notEqual(image.naturalWidth, 0); // need to check dimension is not 0
assert.notEqual(image.naturalHeight, 0); // need to check dimension is not 0

and please add given, when, then annotation.

console.log(msg);
done();
});
console.log(msg);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing assertion statement.
Calling done method means only the test is ended.

And should not call done method only in the reject callback.
If the reject callback is not called, this test will not end forever.

// Given
// When
this.inst = new ImageLoader();

this.inst.setImage("https://s.pstatic.net/static/www/mobile/edit/2015/0318/mobile_110629401121.png").then((img) => {
return this.inst.set("./images/PanoViewer/waterpark_preview.jpg").then(img => {
assert.isOk(img.complete);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing to call done method in asynchronous testing.

const inst = new ImageLoader(imgObj); // image is not loaded.

// Then
expect(inst._loadStatus).to.be.equal(1); // 2 is LOADING status
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we make this case without checking private property?

Because making change to _loadStatus will make this test break.

inst = new ImageLoader(imgObj);

// Then
expect(inst._loadStatus).to.be.equal(2); // 2 is LOADED status
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using private property, too.


it("should resolve image after image is loaded successfully", () => {
// Given
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this test cannot guarantee the image object delivered with resolve callback is fully loaded. It guarantees only resolve callback is calling.

How about below?

it("should resolve image after image is loaded successfully", (done) => {
	// Given
	// When
	const inst = new ImageLoader("./images/PanoViewer/waterpark_preview.jpg");

	inst.get()
	.then((image) => {
		// then
		const isLoaded = image && image.naturalWidth !== 0;
		expect(isLoaded, "resolve image after image has been loaded.").to.be.true;
		done();
	});
});

expect(this.inst).to.be.exist;
this.inst.get().then(null, (msg)=> {
console.log(msg);
done();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if resolve is not called, test will not end.

this.inst.get().then(video => {
assert.isOk(video);
assert.isTrue(video instanceof HTMLVideoElement);
done();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't suggest place done method inside of resolve callback.
What do you think about using Promise.prototype.finally?

change aync test can be stopped

Ref naver#44
@coveralls
Copy link

Coverage Status

Coverage increased (+16.8%) to 54.206% when pulling c4c0b01 on jongmoon:video#44 into 98f6af5 on naver:video#44.

@happyhj
Copy link
Contributor

happyhj commented Dec 1, 2017

LGTM

@jongmoon jongmoon merged commit 3111655 into naver:video#44 Dec 1, 2017
@jongmoon jongmoon mentioned this pull request Mar 19, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants