{"id":72,"date":"2026-03-16T09:16:41","date_gmt":"2026-03-16T09:16:41","guid":{"rendered":"https:\/\/tools.sanepo.com\/?post_type=tool&#038;p=72"},"modified":"2026-03-16T09:16:42","modified_gmt":"2026-03-16T09:16:42","slug":"online-video-cropper","status":"publish","type":"tool","link":"https:\/\/tools.sanepo.com\/zh-hans\/features\/online-video-cropper\/","title":{"rendered":"Free Online Video Cropper \u2013 Secure Client-Side Video Resizer"},"content":{"rendered":"\n<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>Sanepo Video Cropper &#038; Trimmer Pro (Hybrid Engine)<\/title>\n    <!-- Load FFmpeg.wasm from CDN -->\n    <script src=\"https:\/\/unpkg.com\/@ffmpeg\/ffmpeg@0.11.6\/dist\/ffmpeg.min.js\"><\/script>\n    <style>\n        @import url('https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&display=swap');\n\n        \/* Chameleon Universal Theme - Adapts to WP Theme *\/\n        #wpnt-video-cropper {\n            --bg-glass: rgba(0, 0, 0, 0.03);\n            --bg-glass-hover: rgba(0, 0, 0, 0.06);\n            --border-color: rgba(0, 0, 0, 0.1);\n            --text-main: inherit;\n            --accent-color: #4361ee;\n            --accent-hover: #3a56d4;\n            --wpnt-success: #10b981;\n            --wpnt-danger: #ef4444;\n            --wpnt-warning: #f59e0b;\n            --wpnt-radius-lg: 16px;\n            --wpnt-radius-md: 12px;\n            --wpnt-radius-sm: 8px;\n            --card-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);\n            --transition: all 0.3s ease;\n            \n            font-family: 'Inter', sans-serif;\n            color: var(--text-main);\n            width: 100%;\n            margin: 30px auto;\n            background: var(--bg-glass);\n            border: 1px solid var(--border-color);\n            border-radius: var(--wpnt-radius-lg);\n            box-shadow: var(--card-shadow);\n            padding: 32px;\n            box-sizing: border-box;\n            line-height: 1.5;\n            backdrop-filter: blur(12px);\n            -webkit-backdrop-filter: blur(12px);\n        }\n\n        #wpnt-video-cropper * { box-sizing: border-box; transition: var(--transition); }\n\n        #wpnt-video-cropper.wpnt-is-dark {\n            --bg-glass: rgba(255, 255, 255, 0.05);\n            --bg-glass-hover: rgba(255, 255, 255, 0.1);\n            --border-color: rgba(255, 255, 255, 0.15);\n        }\n\n        \/* Header *\/\n        #wpnt-video-cropper .wpnt-header { text-align: center; margin-bottom: 24px; }\n        #wpnt-video-cropper h3 { margin: 0 0 8px 0; font-size: 1.75rem; font-weight: 700; }\n        #wpnt-video-cropper p { margin: 0; font-size: 0.95rem; opacity: 0.8; }\n        \n        #wpnt-video-cropper .wpnt-badges { display: flex; justify-content: center; gap: 10px; margin-top: 12px; flex-wrap: wrap; }\n        #wpnt-video-cropper .wpnt-badge {\n            display: inline-flex; align-items: center; gap: 6px;\n            padding: 6px 14px; border-radius: 20px; font-size: 0.8rem; font-weight: 600;\n        }\n        #wpnt-video-cropper .badge-privacy { background: rgba(16, 185, 129, 0.1); color: var(--wpnt-success); border: 1px solid rgba(16, 185, 129, 0.2); }\n        #wpnt-video-cropper .badge-pro { background: rgba(67, 97, 238, 0.1); color: var(--accent-color); border: 1px solid rgba(67, 97, 238, 0.2); }\n\n        \/* Dropzone *\/\n        #wpnt-video-cropper .wpnt-dropzone {\n            border: 2px dashed var(--accent-color);\n            border-radius: var(--wpnt-radius-lg);\n            padding: 48px 24px; text-align: center; cursor: pointer;\n            background: var(--bg-glass); margin-bottom: 24px;\n        }\n        #wpnt-video-cropper .wpnt-dropzone:hover { background: var(--bg-glass-hover); transform: scale(0.99); }\n        #wpnt-video-cropper .wpnt-dropzone input { display: none; }\n        #wpnt-video-cropper .wpnt-icon-upload { color: var(--accent-color); margin-bottom: 16px; width: 48px; height: 48px; }\n\n        \/* Workspace Grid *\/\n        #wpnt-video-cropper .wpnt-workspace { display: none; gap: 24px; }\n        @media (min-width: 768px) { #wpnt-video-cropper .wpnt-workspace { display: none; grid-template-columns: 2fr 1fr; } }\n\n        \/* Video Area: Shrink-wraps the actual video dimensions perfectly *\/\n        #wpnt-video-cropper .video-wrapper {\n            border-radius: var(--wpnt-radius-md);\n            overflow: hidden; \n            position: relative; \n            display: inline-block;\n            max-width: 100%;\n            line-height: 0; \/* Removes bottom spacing issue *\/\n            box-shadow: var(--card-shadow);\n        }\n        \n        #wpnt-video-cropper video { \n            max-width: 100%; \n            max-height: 480px; \n            width: auto;\n            height: auto;\n            display: block; \n        }\n\n        \/* Pro Cropper Overlay *\/\n        #wpnt-video-cropper .crop-overlay {\n            position: absolute; top: 0; left: 0; width: 100%; height: 100%;\n            border: 2px solid #fff; box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.6);\n            cursor: move; z-index: 10;\n        }\n        \n        #wpnt-video-cropper .crop-grid {\n            position: absolute; inset: 0; pointer-events: none;\n            background-image: \n                linear-gradient(to right, transparent 33.33%, rgba(255,255,255,0.4) 33.33%, rgba(255,255,255,0.4) 33.6%, transparent 33.6%, transparent 66.66%, rgba(255,255,255,0.4) 66.66%, rgba(255,255,255,0.4) 66.9%, transparent 66.9%),\n                linear-gradient(to bottom, transparent 33.33%, rgba(255,255,255,0.4) 33.33%, rgba(255,255,255,0.4) 33.6%, transparent 33.6%, transparent 66.66%, rgba(255,255,255,0.4) 66.66%, rgba(255,255,255,0.4) 66.9%, transparent 66.9%);\n        }\n\n        #wpnt-video-cropper .crop-handle {\n            position: absolute; width: 16px; height: 16px; background: var(--accent-color);\n            border: 2px solid #fff; border-radius: 50%;\n        }\n        #wpnt-video-cropper .handle-se { right: -8px; bottom: -8px; cursor: nwse-resize; }\n        #wpnt-video-cropper .handle-nw { left: -8px; top: -8px; cursor: nwse-resize; }\n\n        \/* Controls Panel *\/\n        #wpnt-video-cropper .wpnt-panel {\n            background: var(--bg-glass); border: 1px solid var(--border-color);\n            border-radius: var(--wpnt-radius-md); padding: 20px;\n            display: flex; flex-direction: column; gap: 20px;\n        }\n\n        #wpnt-video-cropper .panel-section label {\n            display: block; font-size: 0.8rem; font-weight: 700;\n            text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; opacity: 0.7;\n        }\n\n        #wpnt-video-cropper .btn-group { display: flex; flex-wrap: wrap; gap: 8px; }\n        \n        #wpnt-video-cropper .wpnt-btn {\n            background: var(--accent-color); color: #fff; border: none;\n            padding: 10px 16px; border-radius: var(--wpnt-radius-sm);\n            cursor: pointer; font-weight: 600; font-size: 0.9rem;\n            display: inline-flex; align-items: center; justify-content: center; gap: 8px;\n        }\n        #wpnt-video-cropper .wpnt-btn.outline {\n            background: transparent; border: 1px solid var(--border-color); color: inherit;\n        }\n        #wpnt-video-cropper .wpnt-btn.outline.active {\n            border-color: var(--accent-color); color: var(--accent-color); background: rgba(67, 97, 238, 0.1);\n        }\n        #wpnt-video-cropper .wpnt-btn:hover:not(:disabled) { transform: translateY(-2px); }\n        #wpnt-video-cropper .wpnt-btn:disabled { opacity: 0.5; cursor: not-allowed; }\n\n        \/* Range Sliders *\/\n        #wpnt-video-cropper .trimmer-container { position: relative; padding-top: 10px; margin-bottom: 10px;}\n        #wpnt-video-cropper input[type=range] {\n            -webkit-appearance: none; width: 100%; background: transparent; position: absolute; left: 0; pointer-events: none;\n        }\n        #wpnt-video-cropper input[type=range]::-webkit-slider-thumb {\n            -webkit-appearance: none; pointer-events: all; width: 16px; height: 16px;\n            background: var(--accent-color); border-radius: 50%; cursor: ew-resize; margin-top: -6px;\n        }\n        #wpnt-video-cropper .track-bg {\n            width: 100%; height: 6px; background: var(--border-color); border-radius: 4px; position: relative;\n        }\n        #wpnt-video-cropper .track-fill {\n            position: absolute; height: 100%; background: var(--accent-color); border-radius: 4px;\n        }\n        #wpnt-video-cropper .time-display { display: flex; justify-content: space-between; font-size: 0.8rem; margin-top: 25px; font-weight: 600;}\n\n        \/* Select Input *\/\n        #wpnt-video-cropper select {\n            width: 100%; padding: 10px; border-radius: var(--wpnt-radius-sm);\n            border: 1px solid var(--border-color); background: var(--bg-glass);\n            color: var(--text-main); font-family: inherit; font-size: 0.9rem; cursor: pointer;\n        }\n\n        \/* Progress & Results *\/\n        #wpnt-video-cropper .progress-container { display: none; margin-top: 20px; text-align: center; }\n        #wpnt-video-cropper .progress-bar {\n            width: 100%; height: 8px; background: var(--border-color);\n            border-radius: 4px; overflow: hidden; margin-top: 10px;\n        }\n        #wpnt-video-cropper .progress-fill { height: 100%; width: 0%; background: var(--wpnt-success); transition: width 0.1s linear;}\n        \n        #wpnt-video-cropper .results-area {\n            display: none; margin-top: 24px; padding: 24px;\n            background: rgba(16, 185, 129, 0.05); border: 1px solid var(--wpnt-success);\n            border-radius: var(--wpnt-radius-md); text-align: center;\n        }\n        \n        #wpnt-video-cropper .preview-container {\n            margin: 15px auto; max-width: 350px; border-radius: var(--wpnt-radius-sm);\n            overflow: hidden; border: 2px solid var(--border-color); background: #000;\n        }\n\n        @media (max-width: 767px) {\n            #wpnt-video-cropper .wpnt-workspace { display: none; flex-direction: column; }\n        }\n    <\/style>\n<\/head>\n<body>\n\n<div id=\"wpnt-video-cropper\">\n    <div class=\"wpnt-header\">\n        <h3>Sanepo Video Cropper &#038; Trimmer<\/h3>\n        <p>Professional video editing right in your browser.<\/p>\n        <div class=\"wpnt-badges\">\n            <span class=\"wpnt-badge badge-privacy\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"3\" y=\"11\" width=\"18\" height=\"11\" rx=\"2\" ry=\"2\"><\/rect><path d=\"M7 11V7a5 5 0 0 1 10 0v4\"><\/path><\/svg>\n                100% Client-Side\n            <\/span>\n            <span class=\"wpnt-badge badge-pro\" id=\"engineBadge\">\n                <svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polygon points=\"13 2 3 14 12 14 11 22 21 10 12 10 13 2\"><\/polygon><\/svg>\n                Hybrid Engine\n            <\/span>\n        <\/div>\n    <\/div>\n\n    <div class=\"wpnt-dropzone\" id=\"dropzone\">\n        <svg class=\"wpnt-icon-upload\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\">\n            <path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M17 8l-5-5-5 5M12 3v12\"\/>\n        <\/svg>\n        <h4 style=\"margin: 0 0 8px 0;\">Click or Drag Video Here<\/h4>\n        <p style=\"font-size: 0.85rem; opacity: 0.7;\">Supports all major video formats.<\/p>\n        <input type=\"file\" id=\"fileInput\" accept=\"video\/*\">\n    <\/div>\n\n    <div class=\"wpnt-workspace\" id=\"workspace\">\n        <div style=\"display: flex; flex-direction: column; gap: 16px; align-items: center; width: 100%;\">\n            <div class=\"video-wrapper\" id=\"videoContainer\">\n                <video id=\"mainVideo\" muted playsinline><\/video>\n                <div class=\"crop-overlay\" id=\"cropOverlay\">\n                    <div class=\"crop-grid\"><\/div>\n                    <div class=\"crop-handle handle-nw\" id=\"resizeHandleNW\"><\/div>\n                    <div class=\"crop-handle handle-se\" id=\"resizeHandleSE\"><\/div>\n                <\/div>\n            <\/div>\n            \n            <div style=\"display: flex; gap: 10px; justify-content: center; width: 100%;\">\n                <button class=\"wpnt-btn outline\" id=\"playPauseBtn\" style=\"padding: 8px 12px;\">\n                    <svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polygon points=\"5 3 19 12 5 21 5 3\"><\/polygon><\/svg>\n                <\/button>\n            <\/div>\n        <\/div>\n\n        <div class=\"wpnt-panel\">\n            <div class=\"panel-section\">\n                <label>1. Crop Area<\/label>\n                <div class=\"btn-group\" id=\"ratioButtons\">\n                    <button class=\"wpnt-btn outline active\" onclick=\"setRatio(0, this)\">Original\/Free<\/button>\n                    <button class=\"wpnt-btn outline\" onclick=\"setRatio(1, this)\">1:1 (IG)<\/button>\n                    <button class=\"wpnt-btn outline\" onclick=\"setRatio(9\/16, this)\">9:16 (TikTok)<\/button>\n                    <button class=\"wpnt-btn outline\" onclick=\"setRatio(16\/9, this)\">16:9 (YT)<\/button>\n                <\/div>\n                <div style=\"font-size: 0.8rem; margin-top: 10px; opacity: 0.7;\" id=\"resDisplay\">Resolution: Auto<\/div>\n            <\/div>\n\n            <div class=\"panel-section\">\n                <label>2. Trim Duration<\/label>\n                <div class=\"trimmer-container\">\n                    <div class=\"track-bg\"><div class=\"track-fill\" id=\"trackFill\"><\/div><\/div>\n                    <input type=\"range\" id=\"trimStart\" min=\"0\" max=\"100\" value=\"0\" step=\"0.1\">\n                    <input type=\"range\" id=\"trimEnd\" min=\"0\" max=\"100\" value=\"100\" step=\"0.1\">\n                <\/div>\n                <div class=\"time-display\">\n                    <span id=\"timeStartDisplay\">00:00<\/span>\n                    <span id=\"timeEndDisplay\">00:00<\/span>\n                <\/div>\n            <\/div>\n\n            <div class=\"panel-section\">\n                <label>3. Audio Settings<\/label>\n                <select id=\"audioSetting\">\n                    <option value=\"keep\" selected>\ud83d\udd0a Keep Original Audio<\/option>\n                    <option value=\"mute\">\ud83d\udd07 Mute Audio<\/option>\n                <\/select>\n            <\/div>\n\n            <div style=\"margin-top: auto;\">\n                <button class=\"wpnt-btn\" id=\"exportBtn\" style=\"width: 100%; font-size: 1rem; padding: 14px;\">\n                    <svg width=\"20\" height=\"20\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><path d=\"M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4M7 10l5 5 5-5M12 15V3\"\/><\/svg>\n                    Process Video\n                <\/button>\n            <\/div>\n        <\/div>\n    <\/div>\n\n    <div class=\"progress-container\" id=\"progressContainer\">\n        <h4 id=\"progressLabel\" style=\"margin: 0;\">Initializing Engine&#8230;<\/h4>\n        <p style=\"font-size: 0.8rem; opacity: 0.7; margin: 4px 0 0 0;\">Please do not close this tab.<\/p>\n        <div class=\"progress-bar\"><div class=\"progress-fill\" id=\"progressFill\"><\/div><\/div>\n        <p id=\"ffmpegLog\" style=\"font-size: 0.75rem; font-family: monospace; color: var(--accent-color); margin-top: 10px; opacity: 0.8; height: 1.5em; overflow: hidden;\"><\/p>\n    <\/div>\n\n    <div class=\"results-area\" id=\"resultsArea\">\n        <h3 style=\"margin: 0 0 10px 0; color: var(--wpnt-success);\">\u2705 Processing Complete!<\/h3>\n        <p style=\"font-size: 0.9rem; margin-bottom: 10px;\">Your video has been successfully generated.<\/p>\n        \n        <div class=\"preview-container\">\n            <video id=\"previewVideo\" controls style=\"width: 100%; display: block;\"><\/video>\n        <\/div>\n\n        <div style=\"display: flex; gap: 10px; justify-content: center; flex-wrap: wrap;\">\n            <a id=\"downloadBtn\" class=\"wpnt-btn\">Save Video<\/a>\n            <button onclick=\"location.reload()\" class=\"wpnt-btn outline\">Process Another<\/button>\n        <\/div>\n    <\/div>\n<\/div>\n\n<script>\n(function() {\n    \/\/ DOM Elements Initialization\n    const container = document.getElementById('wpnt-video-cropper');\n    const dropzone = document.getElementById('dropzone');\n    const fileInput = document.getElementById('fileInput');\n    const workspace = document.getElementById('workspace');\n    const mainVideo = document.getElementById('mainVideo');\n    const cropOverlay = document.getElementById('cropOverlay');\n    const resizeHandleSE = document.getElementById('resizeHandleSE');\n    const resizeHandleNW = document.getElementById('resizeHandleNW');\n    const videoContainer = document.getElementById('videoContainer');\n    const exportBtn = document.getElementById('exportBtn');\n    const progressContainer = document.getElementById('progressContainer');\n    const progressFill = document.getElementById('progressFill');\n    const progressLabel = document.getElementById('progressLabel');\n    const ffmpegLog = document.getElementById('ffmpegLog');\n    const resultsArea = document.getElementById('resultsArea');\n    const downloadBtn = document.getElementById('downloadBtn');\n    const playPauseBtn = document.getElementById('playPauseBtn');\n    const resDisplay = document.getElementById('resDisplay');\n    const previewVideo = document.getElementById('previewVideo');\n    \n    const trimStart = document.getElementById('trimStart');\n    const trimEnd = document.getElementById('trimEnd');\n    const trackFill = document.getElementById('trackFill');\n    const timeStartDisplay = document.getElementById('timeStartDisplay');\n    const timeEndDisplay = document.getElementById('timeEndDisplay');\n    const audioSetting = document.getElementById('audioSetting');\n\n    let isDragging = false, isResizing = false, resizeDir = '';\n    let startX, startY, startLeft, startTop, startWidth, startHeight;\n    let currentAspectRatio = 0, videoDuration = 0;\n    \n    \/\/ FFmpeg Initialization (Safe Mode)\n    let ffmpeg = null;\n    if (typeof FFmpeg !== 'undefined') {\n        const { createFFmpeg } = FFmpeg;\n        ffmpeg = createFFmpeg({ \n            log: true,\n            corePath: 'https:\/\/unpkg.com\/@ffmpeg\/core@0.11.0\/dist\/ffmpeg-core.js',\n        });\n    }\n\n    \/\/ Chameleon Theme Engine\n    function adaptTheme() {\n        const rgb = window.getComputedStyle(document.body).color.match(\/\\d+\/g);\n        if (rgb) {\n            const brightness = Math.round(((parseInt(rgb[0]) * 299) + (parseInt(rgb[1]) * 587) + (parseInt(rgb[2]) * 114)) \/ 1000);\n            brightness > 125 ? container.classList.add('wpnt-is-dark') : container.classList.remove('wpnt-is-dark');\n        }\n    }\n    window.addEventListener('load', adaptTheme);\n    adaptTheme();\n    new MutationObserver(adaptTheme).observe(document.body, { attributes: true, attributeFilter: ['class', 'style'] });\n\n    \/\/ File Handling\n    dropzone.addEventListener('click', () => fileInput.click());\n    dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.style.borderColor = 'var(--accent-hover)'; });\n    dropzone.addEventListener('dragleave', () => dropzone.style.borderColor = 'var(--accent-color)');\n    dropzone.addEventListener('drop', (e) => { e.preventDefault(); handleFile({ target: { files: e.dataTransfer.files }}); });\n    fileInput.addEventListener('change', handleFile);\n\n    function handleFile(e) {\n        const file = e.target.files[0];\n        if (!file || !file.type.startsWith('video\/')) return alert(\"Please upload a valid video file.\");\n        \n        mainVideo.src = URL.createObjectURL(file);\n        mainVideo.onloadedmetadata = () => {\n            dropzone.style.display = 'none';\n            workspace.style.display = window.innerWidth > 767 ? 'grid' : 'flex';\n            videoDuration = mainVideo.duration;\n            trimStart.max = videoDuration; trimEnd.max = videoDuration;\n            trimStart.value = 0; trimEnd.value = videoDuration;\n            updateTrimmerUI(); \n            resetOverlay(); \n        };\n    }\n\n    \/\/ Video & Trimmer Controls\n    playPauseBtn.addEventListener('click', () => {\n        if(mainVideo.paused) { \n            mainVideo.play(); \n            playPauseBtn.innerHTML = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><rect x=\"6\" y=\"4\" width=\"4\" height=\"16\"><\/rect><rect x=\"14\" y=\"4\" width=\"4\" height=\"16\"><\/rect><\/svg>';\n        } else { \n            mainVideo.pause(); \n            playPauseBtn.innerHTML = '<svg width=\"18\" height=\"18\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\"><polygon points=\"5 3 19 12 5 21 5 3\"><\/polygon><\/svg>';\n        }\n    });\n\n    mainVideo.addEventListener('timeupdate', () => {\n        if(mainVideo.currentTime > parseFloat(trimEnd.value)) mainVideo.currentTime = parseFloat(trimStart.value);\n    });\n\n    function formatTime(s) {\n        return `${Math.floor(s \/ 60).toString().padStart(2, '0')}:${Math.floor(s % 60).toString().padStart(2, '0')}`;\n    }\n\n    function updateTrimmerUI() {\n        let s = parseFloat(trimStart.value), e = parseFloat(trimEnd.value);\n        if (s >= e) { trimStart.value = e - 0.5; s = e - 0.5; }\n        if (e <= s) { trimEnd.value = s + 0.5; e = s + 0.5; }\n        timeStartDisplay.textContent = formatTime(s); timeEndDisplay.textContent = formatTime(e);\n        trackFill.style.left = (s \/ videoDuration) * 100 + '%';\n        trackFill.style.width = ((e - s) \/ videoDuration) * 100 + '%';\n        mainVideo.currentTime = s;\n    }\n\n    trimStart.addEventListener('input', updateTrimmerUI);\n    trimEnd.addEventListener('input', updateTrimmerUI);\n\n    \/\/ Cropper Overlay Logic\n    function resetOverlay() {\n        \/\/ Sets crop to 100% of the video covering the exact dimensions natively\n        cropOverlay.style.top = '0px'; \n        cropOverlay.style.left = '0px';\n        cropOverlay.style.width = '100%'; \n        cropOverlay.style.height = '100%';\n        currentAspectRatio = 0;\n        \n        document.querySelectorAll('#ratioButtons .wpnt-btn').forEach(btn => btn.classList.remove('active'));\n        document.querySelector('#ratioButtons .wpnt-btn').classList.add('active');\n        \n        \/\/ Timeout ensures the DOM has updated the video wrapper's dimensions\n        setTimeout(updateResDisplay, 50);\n    }\n\n    window.setRatio = function(ratio, btnElement) {\n        currentAspectRatio = ratio;\n        if(btnElement) {\n            document.querySelectorAll('#ratioButtons .wpnt-btn').forEach(btn => btn.classList.remove('active'));\n            btnElement.classList.add('active');\n        }\n        \n        if (ratio !== 0) {\n            let newW = cropOverlay.offsetWidth;\n            let newH = newW \/ ratio;\n            \n            \/\/ Boundary checks to ensure it doesn't overflow container when ratio is applied\n            if (newH + cropOverlay.offsetTop > videoContainer.offsetHeight) {\n                newH = videoContainer.offsetHeight - cropOverlay.offsetTop;\n                newW = newH * ratio;\n            }\n            if (newW + cropOverlay.offsetLeft > videoContainer.offsetWidth) {\n                newW = videoContainer.offsetWidth - cropOverlay.offsetLeft;\n                newH = newW \/ ratio;\n            }\n            \n            cropOverlay.style.width = newW + 'px';\n            cropOverlay.style.height = newH + 'px';\n        } else {\n            \/\/ Revert back to full video bounds if \"Free\/Original\" is pressed\n            cropOverlay.style.top = '0px'; \n            cropOverlay.style.left = '0px';\n            cropOverlay.style.width = '100%'; \n            cropOverlay.style.height = '100%';\n        }\n        updateResDisplay();\n    };\n\n    function updateResDisplay() {\n        if(!mainVideo.videoWidth || mainVideo.offsetWidth === 0) return;\n        const w = Math.round(cropOverlay.offsetWidth * (mainVideo.videoWidth \/ mainVideo.offsetWidth));\n        const h = Math.round(cropOverlay.offsetHeight * (mainVideo.videoHeight \/ mainVideo.offsetHeight));\n        resDisplay.textContent = `Output Resolution: ~${w} x ${h}px`;\n    }\n\n    \/\/ Drag & Resize Events\n    cropOverlay.addEventListener('mousedown', startDrag);\n    cropOverlay.addEventListener('touchstart', startDrag, {passive: false});\n    function startDrag(e) {\n        if (e.target.classList.contains('crop-handle')) return;\n        isDragging = true;\n        const event = e.type === 'touchstart' ? e.touches[0] : e;\n        startX = event.clientX; startY = event.clientY;\n        startLeft = cropOverlay.offsetLeft; startTop = cropOverlay.offsetTop;\n        e.preventDefault();\n    }\n\n    resizeHandleSE.addEventListener('mousedown', (e) => startResize(e, 'SE'));\n    resizeHandleSE.addEventListener('touchstart', (e) => startResize(e, 'SE'), {passive: false});\n    resizeHandleNW.addEventListener('mousedown', (e) => startResize(e, 'NW'));\n    resizeHandleNW.addEventListener('touchstart', (e) => startResize(e, 'NW'), {passive: false});\n    \n    function startResize(e, dir) {\n        isResizing = true; resizeDir = dir;\n        const event = e.type === 'touchstart' ? e.touches[0] : e;\n        startX = event.clientX; startY = event.clientY;\n        startWidth = cropOverlay.offsetWidth; startHeight = cropOverlay.offsetHeight;\n        startLeft = cropOverlay.offsetLeft; startTop = cropOverlay.offsetTop;\n        e.stopPropagation(); e.preventDefault();\n    }\n\n    document.addEventListener('mousemove', onMove);\n    document.addEventListener('touchmove', onMove, {passive: false});\n    function onMove(e) {\n        if (!isDragging && !isResizing) return;\n        const event = e.type.includes('touch') ? e.touches[0] : e;\n        const dx = event.clientX - startX, dy = event.clientY - startY;\n\n        if (isDragging) {\n            cropOverlay.style.left = Math.max(0, Math.min(startLeft + dx, videoContainer.offsetWidth - cropOverlay.offsetWidth)) + 'px';\n            cropOverlay.style.top = Math.max(0, Math.min(startTop + dy, videoContainer.offsetHeight - cropOverlay.offsetHeight)) + 'px';\n        }\n\n        if (isResizing) {\n            let newW, newH, newLeft = startLeft, newTop = startTop;\n            if (resizeDir === 'SE') {\n                newW = startWidth + dx; newH = currentAspectRatio === 0 ? startHeight + dy : newW \/ currentAspectRatio;\n                if (newW + startLeft > videoContainer.offsetWidth) { newW = videoContainer.offsetWidth - startLeft; newH = currentAspectRatio ? newW\/currentAspectRatio : newH; }\n                if (newH + startTop > videoContainer.offsetHeight) { newH = videoContainer.offsetHeight - startTop; if(currentAspectRatio) newW = newH * currentAspectRatio; }\n            } else if (resizeDir === 'NW') {\n                newW = startWidth - dx; newH = currentAspectRatio === 0 ? startHeight - dy : newW \/ currentAspectRatio;\n                newLeft = startLeft + dx; newTop = startTop + (startHeight - newH);\n                if (newLeft < 0) { newW += newLeft; newLeft = 0; newH = currentAspectRatio ? newW\/currentAspectRatio : newH; newTop = startTop + startHeight - newH;}\n                if (newTop < 0) { newH += newTop; newTop = 0; if(currentAspectRatio) { newW = newH * currentAspectRatio; newLeft = startLeft + startWidth - newW; } }\n            }\n            if (newW > 50 && newH > 50) {\n                cropOverlay.style.width = newW + 'px'; cropOverlay.style.height = newH + 'px';\n                cropOverlay.style.left = newLeft + 'px'; cropOverlay.style.top = newTop + 'px';\n            }\n        }\n        updateResDisplay();\n    }\n    document.addEventListener('mouseup', () => isDragging = isResizing = false);\n    document.addEventListener('touchend', () => isDragging = isResizing = false);\n\n    \/\/ ==========================================\n    \/\/ HYBRID EXPORT ENGINE (FFmpeg + Fallback)\n    \/\/ ==========================================\n    \n    \/\/ 1. Native Browser Render (Fallback Engine)\n    async function runFallbackEngine(cropX, cropY, cropW, cropH) {\n        ffmpegLog.textContent = \"Using native browser engine...\";\n        progressLabel.textContent = \"Recording Video...\";\n        \n        const canvas = document.createElement('canvas');\n        canvas.width = cropW; canvas.height = cropH;\n        const ctx = canvas.getContext('2d');\n        const finalStream = new MediaStream();\n        \n        ctx.drawImage(mainVideo, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);\n\n        const canvasStream = canvas.captureStream(30);\n        canvasStream.getVideoTracks().forEach(track => finalStream.addTrack(track));\n\n        const keepAudio = audioSetting.value === 'keep';\n        if(keepAudio) {\n            try {\n                const audioStream = mainVideo.captureStream ? mainVideo.captureStream() : (mainVideo.mozCaptureStream ? mainVideo.mozCaptureStream() : null);\n                if (audioStream && audioStream.getAudioTracks().length > 0) {\n                    audioStream.getAudioTracks().forEach(track => finalStream.addTrack(track));\n                }\n            } catch(e) { console.log(\"Audio capture issue\", e); }\n        }\n\n        let mimeType = '', ext = '';\n        const supportedTypes = ['video\/mp4', 'video\/webm;codecs=vp8,opus', 'video\/webm;codecs=vp9,opus', 'video\/webm'];\n        for (let t of supportedTypes) {\n            if (MediaRecorder.isTypeSupported(t)) { mimeType = t; ext = t.includes('mp4') ? 'mp4' : 'webm'; break; }\n        }\n        if (!mimeType) { mimeType = 'video\/webm'; ext = 'webm'; }\n\n        const recorder = new MediaRecorder(finalStream, { mimeType: mimeType });\n        const chunks = [];\n\n        recorder.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); };\n        \n        recorder.onstop = () => {\n            const blob = new Blob(chunks, { type: mimeType });\n            const url = URL.createObjectURL(blob);\n            \n            downloadBtn.href = url;\n            downloadBtn.download = `sanepo-crop-trim-${Date.now()}.${ext}`;\n            downloadBtn.textContent = `Save Video (${ext.toUpperCase()})`;\n            previewVideo.src = url;\n            \n            resultsArea.style.display = 'block';\n            progressContainer.style.display = 'none';\n        };\n\n        const startTime = parseFloat(trimStart.value);\n        const endTime = parseFloat(trimEnd.value);\n        \n        mainVideo.currentTime = startTime;\n        mainVideo.muted = !keepAudio;\n        \n        mainVideo.onseeked = () => {\n            ctx.drawImage(mainVideo, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);\n            recorder.start();\n            mainVideo.play();\n            \n            const drawFrame = () => {\n                if (mainVideo.paused || mainVideo.ended || mainVideo.currentTime >= endTime) {\n                    if (recorder.state === \"recording\") recorder.stop();\n                    mainVideo.pause();\n                    mainVideo.onseeked = null; \n                    return;\n                }\n                \n                ctx.drawImage(mainVideo, cropX, cropY, cropW, cropH, 0, 0, cropW, cropH);\n                progressFill.style.width = (((mainVideo.currentTime - startTime) \/ (endTime - startTime)) * 100) + '%';\n                requestAnimationFrame(drawFrame);\n            };\n            drawFrame();\n        };\n    }\n\n    \/\/ 2. Main Export Trigger\n    exportBtn.addEventListener('click', async () => {\n        if(exportBtn.disabled) return;\n        exportBtn.disabled = true;\n\n        const scaleX = mainVideo.videoWidth \/ mainVideo.offsetWidth;\n        const scaleY = mainVideo.videoHeight \/ mainVideo.offsetHeight;\n        \n        const cropX = cropOverlay.offsetLeft * scaleX;\n        const cropY = cropOverlay.offsetTop * scaleY;\n        const cropW = cropOverlay.offsetWidth * scaleX;\n        const cropH = cropOverlay.offsetHeight * scaleY;\n\n        workspace.style.display = 'none';\n        progressContainer.style.display = 'block';\n\n        if (typeof SharedArrayBuffer === 'undefined' || !ffmpeg) {\n            console.warn(\"SharedArrayBuffer is missing. Server lacks COOP\/COEP headers. Automatically falling back to Browser Engine.\");\n            ffmpegLog.textContent = \"Server configuration limits detected. Switching to native browser engine...\";\n            ffmpegLog.style.color = \"var(--wpnt-warning)\";\n            await runFallbackEngine(cropX, cropY, cropW, cropH);\n            return;\n        }\n\n        try {\n            if (!ffmpeg.isLoaded()) {\n                progressLabel.textContent = \"Loading Engine Core (~20MB)...\";\n                ffmpeg.setLogger(({ type, message }) => {\n                    if(type === 'fferr' || type === 'ffout') ffmpegLog.textContent = message;\n                });\n                await ffmpeg.load();\n            }\n\n            progressLabel.textContent = \"Rendering Video (Please don't close tab)...\";\n            progressFill.style.width = '0%';\n\n            const file = fileInput.files[0];\n            const fileData = await FFmpeg.fetchFile(file);\n            ffmpeg.FS('writeFile', 'input_video', fileData);\n\n            let cw = Math.floor(cropW);\n            let ch = Math.floor(cropH);\n            if (cw % 2 !== 0) cw -= 1; \n            if (ch % 2 !== 0) ch -= 1;\n            \n            const cx = Math.floor(cropX);\n            const cy = Math.floor(cropY);\n\n            ffmpeg.setProgress(({ ratio }) => {\n                const percent = Math.min(Math.max(ratio * 100, 0), 100);\n                progressFill.style.width = `${percent}%`;\n            });\n\n            const sTime = parseFloat(trimStart.value).toFixed(2);\n            const eTime = parseFloat(trimEnd.value).toFixed(2);\n            \n            const ffmpegArgs = [\n                '-i', 'input_video',\n                '-ss', sTime,\n                '-to', eTime,\n                '-vf', `crop=${cw}:${ch}:${cx}:${cy}`, \n                '-c:v', 'libx264',                     \n                '-preset', 'ultrafast',                \n                '-crf', '23'                           \n            ];\n\n            if (audioSetting.value === 'mute') {\n                ffmpegArgs.push('-an'); \n            } else {\n                ffmpegArgs.push('-c:a', 'aac'); \n            }\n\n            ffmpegArgs.push('output.mp4');\n\n            await ffmpeg.run(...ffmpegArgs);\n\n            const outputData = ffmpeg.FS('readFile', 'output.mp4');\n            const blob = new Blob([outputData.buffer], { type: 'video\/mp4' });\n            const url = URL.createObjectURL(blob);\n\n            downloadBtn.href = url;\n            downloadBtn.download = `sanepo-pro-${Date.now()}.mp4`;\n            downloadBtn.textContent = `Save Video (MP4)`;\n            previewVideo.src = url;\n            \n            progressContainer.style.display = 'none';\n            resultsArea.style.display = 'block';\n\n            ffmpeg.FS('unlink', 'input_video');\n            ffmpeg.FS('unlink', 'output.mp4');\n\n        } catch (error) {\n            console.error(\"FFmpeg Error:\", error);\n            ffmpegLog.textContent = \"FFmpeg encountered an error. Seamlessly switching to fallback engine...\";\n            ffmpegLog.style.color = \"var(--wpnt-danger)\";\n            await runFallbackEngine(cropX, cropY, cropW, cropH);\n        }\n    });\n})();\n<\/script>\n\n<\/body>\n<\/html>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Sanepo Video Cropper &#038; Trimmer Pro (Hybrid Engine) [&hellip;]<\/p>\n","protected":false},"featured_media":0,"template":"","meta":[],"class_list":["post-72","tool","type-tool","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/tool\/72","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/tool"}],"about":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/types\/tool"}],"wp:attachment":[{"href":"https:\/\/tools.sanepo.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=72"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}