I am in the process of moving my project from Ionic 3 to Ionic 4, but I noticed that my autoplay video functionality, which was working in Ionic 3, is no longer working in Ionic 4.
From what I can tell my ContentChildren isn't being populated, despite the videos being present in the view. I have tried using forwardRef as well, but that didn't do anything.
I checked other questions that were similar to this, and none of the provided solutions have worked for me, and most were low-quality answers, so I don't believe this is a duplicate considering the time that has passed since they were asked.
The important packages that I'm using are:
Angular: 7.2.15, #ionic/angular 4.4.0, zone.js 0.8.29, intersection-observer 0.7.0.
Here is the autoplay component
import { Component, ContentChildren, ElementRef, forwardRef, NgZone, OnDestroy, OnInit, QueryList } from '#angular/core';
import { AutoplayVideoDirective } from '../../../directives/autoplay-video.directive';
#Component({
selector: 'autoplay',
template: `<ng-content></ng-content>`
})
export class AutoplayContentComponent implements OnInit, OnDestroy {
#ContentChildren(forwardRef(() => AutoplayVideoDirective),
{
read: ElementRef,
descendants: true,
},
) autoPlayVideoRefs: QueryList<any>;
private intersectionObserver: IntersectionObserver;
private mutationObserver: MutationObserver;
private play: Promise<any>;
constructor(private element: ElementRef,
public ngZone: NgZone) {}
public ngOnInit() {
// we can run this outside the ngZone, no need to trigger change detection
this.ngZone.runOutsideAngular(() => {
this.intersectionObserver = this.getIntersectionObserver();
this.mutationObserver = this.getMutationObserver(this.element.nativeElement);
});
}
// clean things ups
public ngOnDestroy() {
if (this.intersectionObserver) {
this.intersectionObserver.disconnect();
}
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
}
// construct the InterSectionObserver and return it
private getIntersectionObserver() {
// execute the onIntersection on the threshold intersection of 0 and 70%
return new IntersectionObserver(entries => this.onIntersection(entries), {
threshold: [0, 0.70],
});
}
// construct the MutationObserver and return it
private getMutationObserver(containerElement: HTMLElement) {
console.log(containerElement);
// execute the onDomChange
let mutationObserver = new MutationObserver(() => this.onDomChange());
// at the very least, childList, attributes, or characterData
// must be set to true
const config = {attributes: true, characterData: true, childList: true};
// attach the mutation observer to the container element
// and start observing it
mutationObserver.observe(containerElement, config);
return mutationObserver;
}
private onDomChange() {
// when the DOM changes, loop over each element
// we want to observe for its interaction,
// and do observe it
console.log(this.autoPlayVideoRefs);
this.autoPlayVideoRefs.forEach((video: ElementRef) => {
this.checkIfVideosCanLoad(video.nativeElement).then((canPlay) => {
console.log('Video can play: ', canPlay);
});
this.intersectionObserver.observe(video.nativeElement);
});
}
/*
* In low-power mode, videos do not load.
* So this quickly checks to see if videos have the capability of loading.
*/
private async checkIfVideosCanLoad(video: any) {
let canPlay: boolean;
return new Promise((resolve) => {
// A safe timeout of 3 seconds, before we declare that the phone is in low power mode.
let timeout = setTimeout(() => {
canPlay = false;
resolve(canPlay);
}, 3000);
// Loads meta data about the video, but not the whole video itself.
video.onloadeddata = () => {
canPlay = true;
clearTimeout(timeout);
resolve(canPlay);
};
});
}
private onIntersection(entries: IntersectionObserverEntry[]) {
entries.forEach((entry: any) => {
// get the video element
let video = entry.target;
// are we intersecting?
if (!entry.isIntersecting) return;
// play the video if we passed the threshold
// of 0.7 and store the promise so we can safely
// pause it again
if (entry.intersectionRatio >= 0.70) {
if (this.play === undefined) this.play = video.play();
} else if (entry.intersectionRatio < 0.70) {
// no need to pause something if it didn't start playing yet.
if (this.play !== undefined) {
// wait for the promise to resolve, then pause the video
this.play.then(() => {
video.pause();
this.play = undefined;
}).catch(() => {});
}
}
});
}
}
The autoplay directive is pretty simple.
import { Directive } from '#angular/core';
/*
* To be used with the autoplay-content.ts component.
*/
#Directive({
selector: 'video'
})
export class AutoplayVideoDirective {}
As is the component which the autoplay component is being wrapped around. This is called inline-video
<autoplay>
<div tappable (tap)="changeVideoAudio(video?.id)">
<video video playsinline loop [muted]="'muted'" [autoplay]="true" preload="auto" muted="muted"
[poster]="(video?.src | AspectRatio) | videoPoster" [id]="'media-' + video?.id" class="video-media">
<source [src]="video.src | AspectRatio" type="video/mp4" src="">
</video>
</div>
</autoplay>
I'm unsure why this wouldn't work in Ionic 4, but was working in ionic 3... so any help would be much appreciated.
The hierarchy of components is as follows:
Feed Page > Feed Item > Autoplay > Inline Video. Where the autoplay component is wrapped around the inline video component.
edit: I have the AllowInlineMediaPlayback preference set as well, in the config.xml
<preference name="AllowInlineMediaPlayback" value="true" />
Related
What is the difference between createEffect vs #Effect annotation in ngrx?
#Injectable()
export class ContactsEffects {
constructor(
private actions$: Actions,
private contactsService: ContactsService,
private contactsSocket: ContactsSocketService
) {}
destroy$ = createEffect( () => this.actions$.pipe(
ofType(remove),
pluck('id'),
switchMap( id => this.contactsService.destroy(id).pipe(
pluck('id'),
map(id => removeSuccess({id}))
))
));
#Effect()
liveCreate$ = this.contactsSocket.liveCreated$.pipe(
map(contact => createSuccess({contact}))
);
}
#ngrx/effects
createEffect for type safety
As alternative to the #Effect() decorator, NgRx 8 provides the createEffect function. The advantage of using createEffect is that it’s type-safe, if the effect does not return an Observable<Action> it will give compile errors. The option { dispatch: false } still exists for effects that don’t dispatch new Actions, adding this option also removes the restriction that an effect needs to return an Observable<Action>.
Starting from NgRx 8 by default, automatically resubscribe to the effect when this happens. This adds a safety net for where the unhappy paths were missed.
It’s possible to turn this feature off by setting resubscribeOnError to false at the effect level.
example:
login$ = createEffect(() => .....), { resubscribeOnError: false });
I'm using Twillio JS API in my project to display video outputs from multiple sources. This API generates enumeration of DOM video/audio elements that can be attached to the page as follows:
let tracks = TwillioVideo.createLocalTracks({
video: { deviceId: this.state.selectedVideoInput.deviceId },
audio: { deviceId: this.state.selectedAudioInput.deviceId }
}
//Find dom element to attach tracks to
let previewContainer = document.getElementById('local-media')
//Attach all tracks
this.setState({localTracks: tracks})
tracks.forEach(track => previewContainer.appendChild(track.attach()))
track.attach() generates a dom element that can be appended but its not something i can put in React state so it can be rendered like so:
<div id="local-media">{this.state.localTracks.map(track => track.attach()}</div>
If I in fact try to do it i get:
Unhandled Rejection (Invariant Violation): Objects are not valid as a
React child (found: [object HTMLAudioElement]). If you meant to render
a collection of children, use an array instead.
EDIT 1:
I was able to get rid of error by doing this:
{this.state.localTracks.map(track => track.attach().Element)}
but it's not returning renderable html but undefined instead
Twilio developer evangelist here.
The attach method in Twilio Video can take an argument, which is an HTMLMediaElement, and will attach the media to that element.
I would recommend that you create a component you can use to render the media for each media track and then use React refs to get a pointer to the DOM element.
Something like this:
import React, { Component, createRef } from 'react';
class Participant extends Component {
constructor(props) {
super(props);
this.video = createRef();
this.audio = createRef();
this.trackAdded = this.trackAdded.bind(this);
}
trackAdded(track) {
if (track.kind === 'video') {
track.attach(this.video.current);
} else if (track.kind === 'audio') {
track.attach(this.audio.current);
}
}
componentDidMount() {
const videoTrack = Array.from(
this.props.participant.videoTracks.values()
)[0];
if (videoTrack) {
videoTrack.attach(this.video.current);
}
const audioTrack = Array.from(
this.props.participant.audioTracks.values()
)[0];
if (audioTrack) {
audioTrack.attach(this.audio.current);
}
this.props.participant.on('trackAdded', this.trackAdded);
}
render() {
return (
<div className="participant">
<h3>{this.props.participant.identity}</h3>
<video ref={this.video} autoPlay={true} muted={true} />
<audio ref={this.audio} autoPlay={true} muted={true} />
</div>
);
}
}
export default Participant;
Then, for every participant in your chat, you can render one of these component.
Let me know if this helps at all.
I a trying to convert the code in the ML5 image classification example(Link) to my React component, which is as follows:
class App extends Component {
video = document.getElementById('video');
state = {
result :null
}
loop = (classifier) => {
classifier.predict()
.then(results => {
this.setState({result: results[0].className});
this.loop(classifier) // Call again to create a loop
})
}
componentDidMount(){
ml5.imageClassifier('MobileNet', this.video)
.then(classifier => this.loop(classifier))
}
render() {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
this.video.srcObject = stream;
this.video.play();
})
return (
<div className="App">
<video id="video" width="640" height="480" autoplay></video>
</div>
);
}
}
export default App;
However this does not work. The error message says that Unhandled Rejection (TypeError): Cannot set property 'srcObject' of null.
I can imagine video = document.getElementById('video'); is probably not able to grab the element by id. So I tried
class App extends Component {
video_element = <video id="video" width="640" height="480" autoplay></video>;
...
render() {
...
return (
<div className="App">
{video_element}
</div>
);
}
}
Which did not work either. I'm confused of what will be the correct method to implement this?
Any help appreciated, thanks!
I am answering again with an highlight on a slightly different problem rather then the ref one.
There is a huge problem that is causing the awful blinking and constantly failing promise due to an exception... and it is the get of the user media in the render method!
Consider this, every time you set the state, the component is re-rendered. You have a loop that constantly update the state of the component, and that promise keeps failing.
You need to get the user media when the component gets mounted:
componentDidMount() {
navigator.mediaDevices.getUserMedia({ video: true }).then(stream => {
if (this.video.current) {
this.video.current.srcObject = stream;
this.video.current.play();
}
ml5.imageClassifier("MobileNet", this.video.current)
.then(classifier => this.loop(classifier));
});
}
Your render method is then a lot shorter:
render() {
return (
<div className="App">
<video ref={this.video} id="video" width="640" height="480" autoPlay />
</div>
)
}
In the moment App is instantiated the video element doesn't exist yet, but the document.getElementById runs, returning undefined or null. That's why you get:
Cannot set property 'srcObject' of null
Because here:
this.video.srcObject = stream
this.video is null.
This is not the proper way of doing this. You should prepare a reference of the dom element, assign it as a prop and then access the element from there. Something like:
class App extends Component {
video = React.createRef()
...
render() {
navigator.mediaDevices.getUserMedia({ video: true })
.then((stream) => {
if ( this.video.current ) {
this.video.current.srcObject = stream;
this.video.current.play();
}
})
return (
...
<video ref={ this.video }
id="video"
width="640"
height="480"
autoplay
/>
I'm using mobx 4.2.0
When I try a use a computed property, I got some problem
Code like this:
class ODOM {
constructor(props) {
console.log('how many times')
}
#observable speed = 0
#action change(obj) {
console.log(obj)
Object.keys(obj).forEach(item => {
this[item] = obj[item]
})
}
#computed get velocity() {
console.log('entry')
return this.speed*60*60/1000
}
}
const model = new ODOM()
let total = 0
setInterval(() => {
model.change({
speed: ++total
})
}, 3000)
export default model
the console 'entry' only run once
What's the problem with those code
Your computed has to be observed in order for it to be re-computed when the observables it depends on changes.
This example uses an autorun to show the behavior:
class ODOM {
#observable speed = 0
#action change(obj) {
Object.keys(obj).forEach(item => {
this[item] = obj[item]
})
}
#computed get velocity() {
console.log('entry')
return this.speed*60*60/1000
}
}
const model = new ODOM()
let total = 0
setInterval(() => {
model.change({
speed: ++total
})
}, 1000);
autorun(() => {
console.log(model.velocity);
});
Computed re-calculation is only triggered by mobx in components that directly use this property in the render function and these react components must be annotated with #observer attribute.
otherwise use non computed/normal properties or cache the value yourself.
if #withRouter is under #observer will make #observer,#computed failure
like this (wrong):
#observer
// #ts-ignore
#withRouter
you need to write like this (correct):
// #ts-ignore
#withRouter
#observer
I found the problem.Cause the observer doesn't use correctly.
I am trying to call to a service on input key-up event.
The HTML
<input placeholder="enter name" (keyup)='onKeyUp($event)'>
Below is the onKeyUp() function
onKeyUp(event) {
let observable = Observable.fromEvent(event.target, 'keyup')
.map(value => event.target.value)
.debounceTime(1000)
.distinctUntilChanged()
.flatMap((search) => {
// call the service
});
observable.subscribe((data) => {
// data
});
}
It was found from the network tab of the browser that, it is calling the key-up function on every key-up event(as it is supposed to do), but what I am trying to achieve is a debounce time of 1sec between each service call. Also, the event is triggered if I move the arrow key move.
plunkr link
So the chain is really correct but the problem is that you're creating an Observable and subscribe to it on every keyup event. That's why it prints the same value multiple times. There're simply multiple subscriptions which is not what you want to do.
There're obviously more ways to do it correctly, for example:
#Component({
selector: 'my-app',
template: `
<div>
<input type="text" (keyup)='keyUp.next($event)'>
</div>
`,
})
export class App implements OnDestroy {
public keyUp = new Subject<KeyboardEvent>();
private subscription: Subscription;
constructor() {
this.subscription = this.keyUp.pipe(
map(event => event.target.value),
debounceTime(1000),
distinctUntilChanged(),
mergeMap(search => of(search).pipe(
delay(500),
)),
).subscribe(console.log);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
See your updated demo: http://plnkr.co/edit/mAMlgycTcvrYf7509DOP
Jan 2019: Updated for RxJS 6
#marlin has given a great solution and it works fine in angular 2.x but with angular 6 they have started to use rxjs 6.0 version and that has some slight different syntax so here is the updated solution.
import {Component} from '#angular/core';
import {Observable, of, Subject} from 'rxjs';
import {debounceTime, delay, distinctUntilChanged, flatMap, map, tap} from 'rxjs/operators';
#Component({
selector: 'my-app',
template: `
<div>
<input type="text" (keyup)='keyUp.next($event)'>
</div>
`,
})
export class AppComponent {
name: string;
public keyUp = new Subject<string>();
constructor() {
const subscription = this.keyUp.pipe(
map(event => event.target.value),
debounceTime(1000),
distinctUntilChanged(),
flatMap(search => of(search).pipe(delay(500)))
).subscribe(console.log);
}
}
Well, here's a basic debouncer.
ngOnInit ( ) {
let inputBox = this.myInput.nativeElement;
let displayDiv = this.myDisplay.nativeElement;
let source = Rx.Observable.fromEvent ( inputBox, 'keyup' )
.map ( ( x ) => { return x.currentTarget.value; } )
.debounce ( ( x ) => { return Rx.Observable.timer ( 1000 ); } );
source.subscribe (
( x ) => { displayDiv.innerText = x; },
( err ) => { console.error ( 'Error: %s', err ) },
() => {} );
}
}
If you set up the two indicated view children (the input and the display), you'll see the debounce work. Not sure if this doesn't do anything your does, but this basic form is (as far as I know) the straightforward way to debounce, I use this starting point quite a bit, the set of the inner text is just a sample, it could make a service call or whatever else you need it to do.
I would suggest that you check the Angular2 Tutorial that show a clean and explained example of something similar to what you're asking.
https://angular.io/docs/ts/latest/tutorial/toh-pt6.html#!#observables