-
Notifications
You must be signed in to change notification settings - Fork 30
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d1b28e8
Showing
78 changed files
with
1,407 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
# Specifies intentionally untracked files to ignore when using Git | ||
# http:https://git-scm.com/docs/gitignore | ||
|
||
*~ | ||
*.sw[mnpcod] | ||
*.log | ||
*.tmp | ||
*.tmp.* | ||
log.txt | ||
*.sublime-project | ||
*.sublime-workspace | ||
|
||
.idea/ | ||
.sass-cache/ | ||
.versions/ | ||
coverage/ | ||
dist/ | ||
node_modules/ | ||
tmp/ | ||
temp/ | ||
www/build/ | ||
$RECYCLE.BIN/ | ||
|
||
.DS_Store | ||
Thumbs.db | ||
UserInterfaceState.xcuserstate | ||
|
||
platforms/ | ||
plugins/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
## VideoEditor: An Ionic 2 App :camera: :video_camera: :scissors: | ||
|
||
This is an Ionic 2 app that shows how to use the great [cordova-plugin-video-editor](https://github.com/jbavari/cordova-plugin-video-editor) and [cordova-plugin-instagram-assets-picker](https://github.com/rossmartin/cordova-plugin-instagram-assets-picker) plugins. It allows transcoding a video with every possible setting the video editor plugin provides as well as trimming, creating thumbnails, and getting video info (width, height orientation, duration, size, & bitrate). | ||
|
||
![GIF](example.gif) | ||
|
||
Watch the video [here](https://youtu.be/WddM6xNlOuA). | ||
|
||
1. Install the the latest beta version of the Ionic CLI: | ||
``` | ||
npm install -g ionic@beta | ||
``` | ||
|
||
1. Clone this repository | ||
``` | ||
git clone https://github.com/rossmartin/video-editor-ionic2 | ||
``` | ||
|
||
1. Navigate to the app directory: | ||
``` | ||
cd video-editor-ionic2 | ||
``` | ||
|
||
1. Install the dependencies | ||
``` | ||
npm install | ||
``` | ||
|
||
From here you can build and run the app on different platforms using the traditional Ionic commands (`ionic build ios`, etc.) | ||
|
||
I have only tested this app on iOS but it should run fine on Android. Transcoding videos on Android will be much slower because the video editor plugin uses ffmpeg on Android. I have had terrible luck with HTML5 videos in the past on Android as well so YMMV there also. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import {App, Platform} from 'ionic/ionic'; | ||
import {TabsPage} from './pages/tabs/tabs'; | ||
|
||
|
||
@App({ | ||
template: '<ion-nav [root]="rootPage"></ion-nav>', | ||
config: {} // http:https://ionicframework.com/docs/v2/api/config/Config/ | ||
}) | ||
export class MyApp { | ||
constructor(platform: Platform) { | ||
this.rootPage = TabsPage; | ||
|
||
platform.ready().then(() => { | ||
if (window.cordova) { | ||
if (window.cordova.plugins && window.cordova.plugins.Keyboard) { | ||
cordova.plugins.Keyboard.hideKeyboardAccessoryBar(true); | ||
cordova.plugins.Keyboard.disableScroll(true); | ||
} | ||
|
||
if (window.StatusBar) { | ||
StatusBar.styleDefault(); | ||
} | ||
} | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
<ion-navbar *navbar> | ||
<ion-title>Edit Video</ion-title> | ||
|
||
<ion-buttons end> | ||
<button (click)="performEdit()">Next</button> | ||
</ion-buttons> | ||
</ion-navbar> | ||
|
||
<ion-content padding class="edit-video"> | ||
|
||
<ion-segment [(ngModel)]="editAction"> | ||
<ion-segment-button value="trim" (click)="onTrimButtonClick($event)"> | ||
<ion-icon name="cut"></ion-icon> | ||
Trim | ||
</ion-segment-button> | ||
<ion-segment-button value="cover" (click)="onCoverFrameButtonClick($event)"> | ||
<ion-icon name="image"></ion-icon> | ||
Cover Frame | ||
</ion-segment-button> | ||
</ion-segment> | ||
|
||
|
||
<div (click)="onVideoClick($event)" style="margin-top: 5px;"> | ||
<video id="edit-video-element" | ||
width="100%" | ||
webkit-playsinline | ||
poster="{{thumbnailPath}}" | ||
src="{{videoPath}}"> | ||
</video> | ||
</div> | ||
|
||
<!-- can't use ngSwitch here --> | ||
|
||
<div [class.hide]="editAction !== 'trim'"> | ||
<div style="margin: 2px 15px 9px 15px;"> | ||
<div id="trim-slider"></div> | ||
</div> | ||
|
||
<div *ngIf="videoDuration"> | ||
Duration: {{videoDuration}} seconds | ||
</div> | ||
</div> | ||
|
||
<div [class.hide]="editAction !== 'cover'"> | ||
<div style="margin: 2px 15px 9px 15px;"> | ||
<div id="cover-frame-slider"></div> | ||
</div> | ||
</div> | ||
|
||
|
||
</ion-content> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,237 @@ | ||
import {Page, NavController, NavParams} from 'ionic/ionic'; | ||
import {NgZone} from 'angular2/core'; | ||
import {VideoResultPage} from '../video-result/video-result'; | ||
|
||
|
||
@Page({ | ||
templateUrl: 'build/pages/edit-video/edit-video.html' | ||
}) | ||
export class EditVideoPage { | ||
constructor(nav:NavController, navParams:NavParams) { | ||
this.zone = new NgZone({ enableLongStackTrace: false }); | ||
this.nav = nav; | ||
this.videoPath = navParams.get('videoPath'); | ||
this.thumbnailPath = navParams.get('thumbnailPath'); | ||
} | ||
|
||
onPageWillEnter() { | ||
this.editAction = this.lastEditAction || 'trim'; | ||
} | ||
|
||
onPageDidEnter() { | ||
setTimeout(() => { | ||
let video = this.video = document.querySelector('#edit-video-element'); | ||
video.play(); | ||
video.pause(); | ||
video.currentTime = this.lastTrimTime || 0; | ||
|
||
// declare shared trim logic vars here | ||
// this is done to safely reference them in setupCoverFrameLogic | ||
// this ensures the cover frame can only be set within the trimmed range | ||
this.videoTrimStart = 0; | ||
this.videoTrimEnd = video.duration; | ||
this.videoDuration = (this.videoTrimEnd - this.videoTrimStart).toFixed(2); | ||
|
||
this.setupTrimLogic(); | ||
this.setupCoverFrameLogic(); | ||
}, 500); | ||
} | ||
|
||
setupTrimLogic() { | ||
let video = this.video; | ||
let trimSlider = this.trimSlider = document.querySelector('#trim-slider'); | ||
let numTrimSliderUpdates = 0; | ||
|
||
if (typeof trimSlider.noUiSlider !== 'undefined') { | ||
console.log('trimSlider already instantiated'); | ||
// no need to recreate the slider it is already in the DOM | ||
// this can happen if coming back here from settings | ||
return; | ||
} | ||
|
||
noUiSlider.create(trimSlider, { | ||
start: [0, video.duration], | ||
limit: video.duration, | ||
tooltips: [true, true], | ||
connect: true, | ||
range: { | ||
min: 0, | ||
max: video.duration | ||
} | ||
}); | ||
|
||
trimSlider.noUiSlider.on('update', (values, handle) => { | ||
// update is called twice before anything actually happens, this fixes it | ||
if (numTrimSliderUpdates < 3) { | ||
numTrimSliderUpdates++; | ||
return; | ||
} | ||
|
||
let value = Number(values[handle]); | ||
|
||
(handle) ? this.videoTrimEnd = value : this.videoTrimStart = value; | ||
|
||
this.videoDuration = (this.videoTrimEnd - this.videoTrimStart).toFixed(2); | ||
|
||
video.currentTime = this.lastTrimTime = value; | ||
}); | ||
} | ||
|
||
setupCoverFrameLogic() { | ||
let video = this.video; | ||
let coverFrameSlider = this.coverFrameSlider = document.querySelector('#cover-frame-slider'); | ||
|
||
if (typeof coverFrameSlider.noUiSlider !== 'undefined') { | ||
console.log('coverFrameSlider already instantiated'); | ||
// no need to recreate the slider it is already in the DOM | ||
// this can happen if coming back here from settings | ||
return; | ||
} | ||
|
||
this.coverFrameTime = this.videoTrimStart; | ||
|
||
noUiSlider.create(coverFrameSlider, { | ||
start: this.videoTrimStart, | ||
connect: 'lower', | ||
tooltips: true, | ||
range: { | ||
min: this.videoTrimStart, | ||
max: this.videoTrimEnd | ||
} | ||
}); | ||
|
||
coverFrameSlider.noUiSlider.on('update', (values, handle) => { | ||
let value = Number(values[handle]); | ||
video.currentTime = this.coverFrameTime = value; | ||
}); | ||
} | ||
|
||
onTrimButtonClick(e) { | ||
this.editAction = this.lastEditAction = 'trim'; | ||
this.video.currentTime = this.lastTrimTime || 0; | ||
} | ||
|
||
onCoverFrameButtonClick(e) { | ||
this.editAction = this.lastEditAction = 'cover'; | ||
|
||
if (this.coverFrameTime < this.videoTrimStart) { | ||
this.coverFrameTime = this.videoTrimStart; | ||
} | ||
|
||
if (this.coverFrameTime > this.videoTrimEnd) { | ||
this.coverFrameTime = this.videoTrimEnd; | ||
} | ||
|
||
this.video.currentTime = this.coverFrameTime || 0; | ||
|
||
this.coverFrameSlider.noUiSlider.updateOptions({ | ||
range: { | ||
min: this.videoTrimStart, | ||
max: this.videoTrimEnd | ||
} | ||
}); | ||
} | ||
|
||
onVideoClick(e) { | ||
(this.video.paused) ? this.video.play() : this.video.pause(); | ||
} | ||
|
||
performEdit() { | ||
window.plugins.spinnerDialog.show(null, null, true); | ||
|
||
let ls = window.localStorage; | ||
let options = { | ||
fileUri: this.videoPath, | ||
outputFileName: new Date().getTime(), | ||
atTime: this.coverFrameTime, | ||
thumbnailQuality: ls['thumbnailQuality'] || 100, | ||
thumbnailMaintainAspectRatio: ls['thumbnailMaintainAspectRatio'] || true | ||
}; | ||
let widthOption = ls['thumbnailWidth']; | ||
let heightOption = ls['thumbnailHeight']; | ||
|
||
if (widthOption && widthOption !== 0) { | ||
options.width = widthOption; | ||
} | ||
|
||
if (heightOption && heightOption !== 0) { | ||
options.height = heightOption; | ||
} | ||
|
||
// not sure how to use promises/observables yet in angular 2 | ||
// create the thumbnail, trim the video, then transcode it | ||
|
||
VideoEditor.createThumbnail( | ||
(result) => { | ||
console.log('createThumbnail success, result: ', result); | ||
this.newThumbnailPath = result; | ||
this.trimVideo(); | ||
}, | ||
(err) => { | ||
console.log('createThumbnail error, err: ', err); | ||
}, | ||
options | ||
); | ||
|
||
} | ||
|
||
trimVideo() { | ||
VideoEditor.trim( | ||
(result) => { | ||
console.log('trim success, result: ', result); | ||
this.transcodeVideo(result); | ||
}, | ||
(err) => { | ||
console.log('trim error, err: ', err); | ||
}, | ||
{ | ||
fileUri: this.videoPath, | ||
outputFileName: new Date().getTime(), | ||
trimStart: this.videoTrimStart, | ||
trimEnd: this.videoTrimEnd | ||
} | ||
); | ||
} | ||
|
||
transcodeVideo(trimmedVideoPath) { | ||
let ls = window.localStorage; | ||
let widthOption = ls['width']; | ||
let heightOption = ls['height']; | ||
let options = { | ||
fileUri: trimmedVideoPath, | ||
outputFileName: new Date().getTime(), | ||
outputFileType: VideoEditorOptions.OutputFileType.MPEG4, | ||
videoBitrate: ls['videoBitrate'] || 1000000, // 1 megabit | ||
audioChannels: ls['audioChannels'] || 2, | ||
audioSampleRate: ls['audioSampleRate'] || 44100, | ||
audioBitrate: ls['audioBitrate'] || 128000, | ||
maintainAspectRatio: ls['maintainAspectRatio'] || true, | ||
optimizeForNetworkUse: ls['optimizeForNetworkUse'] || true, | ||
saveToLibrary: ls['saveToLibrary'] || true | ||
}; | ||
|
||
if (widthOption && widthOption !== 0) { | ||
options.width = widthOption; | ||
} | ||
|
||
if (heightOption && heightOption !== 0) { | ||
options.height = heightOption; | ||
} | ||
|
||
VideoEditor.transcodeVideo( | ||
(result) => { | ||
console.log('transcodeVideo success, result: ', result); | ||
window.plugins.spinnerDialog.hide(); | ||
this.nav.push(VideoResultPage, { | ||
videoPath: result, | ||
thumbnailPath: this.newThumbnailPath | ||
}); | ||
}, | ||
(err) => { | ||
console.log('transcodeVideo error, err: ', err); | ||
}, | ||
options | ||
); | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
.noUi-tooltip { | ||
opacity: 0.8 | ||
} | ||
|
||
.noUi-horizontal .noUi-handle-upper .noUi-tooltip { | ||
bottom: 28px !important; | ||
top: -32px !important; | ||
} | ||
|
||
.noUi-connect { | ||
background: map-get($colors, primary) !important; | ||
} | ||
|
||
.edit-video ion-segment-button ion-icon { | ||
position: relative; | ||
top: 2px; | ||
} |
Oops, something went wrong.