Repository: lihaoyun6/QuickRecorder Branch: main Commit: e82051787f01 Files: 88 Total size: 568.0 KB Directory structure: gitextract_xfnzrp5g/ ├── .gitignore ├── LICENSE ├── QuickRecorder/ │ ├── AVContext.swift │ ├── Assets.xcassets/ │ │ ├── AccentColor.colorset/ │ │ │ └── Contents.json │ │ ├── AppIcon.appiconset/ │ │ │ └── Contents.json │ │ ├── Colors/ │ │ │ ├── Contents.json │ │ │ ├── black_white.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── buttonRed.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── buttonRedDark.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── dark_my_red.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── myblue.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── mygreen.colorset/ │ │ │ │ └── Contents.json │ │ │ └── mypurple.colorset/ │ │ │ └── Contents.json │ │ ├── Contents.json │ │ ├── Others/ │ │ │ ├── Contents.json │ │ │ ├── audioIcon.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── camera.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── save.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── unknowScreen.imageset/ │ │ │ │ └── Contents.json │ │ │ └── window.select.imageset/ │ │ │ └── Contents.json │ │ ├── Settings/ │ │ │ ├── Contents.json │ │ │ ├── blacklist.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── film.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── gear.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── hotkey.imageset/ │ │ │ │ └── Contents.json │ │ │ └── record.imageset/ │ │ │ └── Contents.json │ │ └── Surprise/ │ │ ├── ChineseNewYear/ │ │ │ ├── Contents.json │ │ │ ├── fuzi1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── fuzi2.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── fuzi3.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── hongbao1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── hongbao2.imageset/ │ │ │ │ └── Contents.json │ │ │ └── hongbao3.imageset/ │ │ │ └── Contents.json │ │ ├── Christmas/ │ │ │ ├── Contents.json │ │ │ ├── christmasTree1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── christmasTree2.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── snowflake1.imageset/ │ │ │ │ └── Contents.json │ │ │ ├── snowflake2.imageset/ │ │ │ │ └── Contents.json │ │ │ └── snowflake3.imageset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Base.lproj/ │ │ └── Credits.rtf │ ├── Info.plist │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ └── Contents.json │ ├── QuickRecorder.entitlements │ ├── QuickRecorderApp.swift │ ├── RecordEngine.swift │ ├── SCContext.swift │ ├── Supports/ │ │ ├── AppleScript.swift │ │ ├── GroupForm.swift │ │ ├── Scriptable.sdef │ │ ├── SleepPreventer.swift │ │ ├── Sparkle.swift │ │ ├── WindowAccessor.swift │ │ └── WindowHighlighter.swift │ ├── ViewModel/ │ │ ├── AppBlockSelector.swift │ │ ├── AppSelector.swift │ │ ├── AreaSelector.swift │ │ ├── CameraOverlayer.swift │ │ ├── ContentView.swift │ │ ├── ContentViewNew.swift │ │ ├── MousePointer.swift │ │ ├── PreviewView.swift │ │ ├── QmaPlayer.swift │ │ ├── ScreenMagnifier.swift │ │ ├── ScreenSelector.swift │ │ ├── SettingsView.swift │ │ ├── StatusBar.swift │ │ ├── SurpriseView.swift │ │ ├── VideoEditor.swift │ │ ├── WinSelector.swift │ │ └── iDeviceSelector.swift │ ├── it.lproj/ │ │ ├── Credits.rtf │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ ├── zh-Hans.lproj/ │ │ ├── Credits.rtf │ │ ├── InfoPlist.strings │ │ └── Localizable.strings │ └── zh-Hant.lproj/ │ ├── Credits.rtf │ ├── InfoPlist.strings │ └── Localizable.strings ├── QuickRecorder.xcodeproj/ │ ├── project.pbxproj │ ├── project.xcworkspace/ │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata/ │ │ ├── IDEWorkspaceChecks.plist │ │ └── swiftpm/ │ │ └── Package.resolved │ └── xcshareddata/ │ └── xcschemes/ │ └── QuickRecorder.xcscheme ├── README.md ├── README_zh.md └── appcast.xml ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitignore ================================================ # Xcode # # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore ## User settings xcuserdata/ ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) *.xcscmblueprint *.xccheckout ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) build/ DerivedData/ *.moved-aside *.pbxuser !default.pbxuser *.mode1v3 !default.mode1v3 *.mode2v3 !default.mode2v3 *.perspectivev3 !default.perspectivev3 ## Obj-C/Swift specific *.hmap ## App packaging *.ipa *.dSYM.zip *.dSYM ## Playgrounds timeline.xctimeline playground.xcworkspace # Swift Package Manager # # Add this line if you want to avoid checking in source code from Swift Package Manager dependencies. # Packages/ # Package.pins # Package.resolved # *.xcodeproj # # Xcode automatically generates this directory with a .xcworkspacedata file and xcuserdata # hence it is not needed unless you have added a package configuration file to your project # .swiftpm .build/ # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However # you should judge for yourself, the pros and cons are mentioned at: # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control # # Pods/ # # Add this line if you want to avoid checking in source code from the Xcode workspace # *.xcworkspace # Carthage # # Add this line if you want to avoid checking in source code from Carthage dependencies. # Carthage/Checkouts Carthage/Build/ # Accio dependency management Dependencies/ .accio/ # fastlane # # It is recommended to not store the screenshots in the git repo. # Instead, use fastlane to re-generate the screenshots whenever they are needed. # For more information about the recommended setup visit: # https://docs.fastlane.tools/best-practices/source-control/#source-control fastlane/report.xml fastlane/Preview.html fastlane/screenshots/**/*.png fastlane/test_output # Code Injection # # After new code Injection tools there's a generated folder /iOSInjectionProject # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ .DS_Store ================================================ FILE: LICENSE ================================================ GNU AFFERO GENERAL PUBLIC LICENSE Version 3, 19 November 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU Affero General Public License is a free, copyleft license for software and other kinds of works, specifically designed to ensure cooperation with the community in the case of network server software. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, our General Public Licenses are intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. Developers that use our General Public Licenses protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License which gives you legal permission to copy, distribute and/or modify the software. A secondary benefit of defending all users' freedom is that improvements made in alternate versions of the program, if they receive widespread use, become available for other developers to incorporate. Many developers of free software are heartened and encouraged by the resulting cooperation. However, in the case of software used on network servers, this result may fail to come about. The GNU General Public License permits making a modified version and letting the public access it on a server without ever releasing its source code to the public. The GNU Affero General Public License is designed specifically to ensure that, in such cases, the modified source code becomes available to the community. It requires the operator of a network server to provide the source code of the modified version running there to the users of that server. Therefore, public use of a modified version, on a publicly accessible server, gives the public access to the source code of the modified version. An older license, called the Affero General Public License and published by Affero, was designed to accomplish similar goals. This is a different license, not a version of the Affero GPL, but Affero has released a new version of the Affero GPL which permits relicensing under this license. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU Affero General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Remote Network Interaction; Use with the GNU General Public License. Notwithstanding any other provision of this License, if you modify the Program, your modified version must prominently offer all users interacting with it remotely through a computer network (if your version supports such interaction) an opportunity to receive the Corresponding Source of your version by providing access to the Corresponding Source from a network server at no charge, through some standard or customary means of facilitating copying of software. This Corresponding Source shall include the Corresponding Source for any work covered by version 3 of the GNU General Public License that is incorporated pursuant to the following paragraph. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the work with which it is combined will remain governed by version 3 of the GNU General Public License. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU Affero General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU Affero General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU Affero General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU Affero General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If your software can interact with users remotely through a computer network, you should also make sure that it provides a way for users to get its source. For example, if your program is a web application, its interface could display a "Source" link that leads users to an archive of the code. There are many ways you could offer source, and different solutions will be better for different programs; see section 13 for the specific requirements. You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU AGPL, see . ================================================ FILE: QuickRecorder/AVContext.swift ================================================ // // AVContext.swift // QuickRecorder // // Created by apple on 2024/4/27. // import AppKit import Foundation import AVFoundation import UserNotifications extension AppDelegate { func recordingCamera(with device: AVCaptureDevice) { SCContext.captureSession = AVCaptureSession() guard let input = try? AVCaptureDeviceInput(device: device), SCContext.captureSession.canAddInput(input) else { print("Failed to set up camera") SCContext.requestCameraPermission() return } SCContext.captureSession.addInput(input) let videoOutput = AVCaptureVideoDataOutput() videoOutput.setSampleBufferDelegate(self, queue: .global()) if SCContext.captureSession.canAddOutput(videoOutput) { SCContext.captureSession.addOutput(videoOutput) } SCContext.captureSession.startRunning() DispatchQueue.main.async { self.startCameraOverlayer() } } func closeCamera() { if SCContext.isCameraRunning() { //SCContext.previewType = nil if camWindow.isVisible { camWindow.close() } SCContext.captureSession.stopRunning() } } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { /* 保留后续以作他用 if !SCContext.isPaused && ud.string(forKey: "recordCam") != "" { if sampleBuffer.isValid { SCContext.isCameraReady = true } if sampleBuffer.imageBuffer != nil { SCContext.frameCache = sampleBuffer } }*/ } } class AVOutputClass: NSObject, AVCaptureFileOutputRecordingDelegate, AVCaptureVideoDataOutputSampleBufferDelegate { static let shared = AVOutputClass() var output: AVCaptureMovieFileOutput! var dataOutput: AVCaptureVideoDataOutput! //var captureSession: AVCaptureSession! func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { //print(sampleBuffer.nsImage?.size) } public func startRecording(with device: AVCaptureDevice, mute: Bool = false, preset: AVCaptureSession.Preset = .high, didOutput: Bool = true) { output = AVCaptureMovieFileOutput() dataOutput = AVCaptureVideoDataOutput() dataOutput.setSampleBufferDelegate(self, queue: .global()) SCContext.captureSession = AVCaptureSession() SCContext.previewSession = AVCaptureSession() SCContext.captureSession.sessionPreset = preset SCContext.previewSession.sessionPreset = preset guard let input = try? AVCaptureDeviceInput(device: device), let preview = try? AVCaptureDeviceInput(device: device), SCContext.captureSession.canAddInput(input), SCContext.previewSession.canAddInput(preview), SCContext.captureSession.canAddOutput(output), SCContext.previewSession.canAddOutput(dataOutput) else { print("Failed to set up camera or device") SCContext.requestCameraPermission() return } SCContext.captureSession.addInput(input) SCContext.captureSession.addOutput(output) SCContext.previewSession.addInput(preview) SCContext.previewSession.addOutput(dataOutput) if mute { if let audioConnection = output.connection(with: .audio) { SCContext.captureSession.removeConnection(audioConnection) /*DispatchQueue.main.async { let alert = createAlert(title: "No Audio Connection", message: "Unable to get audio stream on this device, only screen content will be recorded!", button1: "OK") alert.runModal() }*/ } } if didOutput { let encoderIsH265 = ud.string(forKey: "encoder") == Encoder.h265.rawValue let videoSettings: [String: Any] = [ AVVideoCodecKey: encoderIsH265 ? AVVideoCodecType.hevc : AVVideoCodecType.h264 ] guard let connection = output.connection(with: .video) else { return } output.setOutputSettings(videoSettings, for: connection) let fileEnding = ud.string(forKey: "videoFormat") ?? "" SCContext.filePath = "\(SCContext.getFilePath()).\(fileEnding)" SCContext.captureSession.startRunning() output.startRecording(to: SCContext.filePath.url, recordingDelegate: self) SCContext.streamType = StreamType.idevice SCContext.startTime = Date.now } SCContext.previewSession.startRunning() DispatchQueue.main.async { closeAllWindow(except: "Area Overlayer".local) updateStatusBar() AppDelegate.shared.startDeviceOverlayer(size: NSSize(width: 300, height: 500)) } } public func stopRecording() { if SCContext.captureSession.isRunning { output.stopRecording() SCContext.captureSession.stopRunning() SCContext.previewSession.stopRunning() SCContext.streamType = nil SCContext.startTime = nil DispatchQueue.main.async { controlPanel.close() deviceWindow.close() updateStatusBar() } } } func closePreview() { if SCContext.isCameraRunning() { //SCContext.previewType = nil if deviceWindow.isVisible { deviceWindow.close() } if let preview = SCContext.previewSession { preview.stopRunning() } } } func fileOutput(_ output: AVCaptureFileOutput, didFinishRecordingTo outputFileURL: URL, from connections: [AVCaptureConnection], error: Error?) { let content = UNMutableNotificationContent() content.title = "Recording Completed".local content.body = String(format: "File saved to: %@".local, outputFileURL.path) content.sound = UNNotificationSound.default let trigger = UNTimeIntervalNotificationTrigger(timeInterval: 1, repeats: false) let request = UNNotificationRequest(identifier: "quickrecorder.completed.\(UUID().uuidString)", content: content, trigger: trigger) UNUserNotificationCenter.current().add(request) { error in if let error = error { print("Notification failed to send:\(error.localizedDescription)") } } } } ================================================ FILE: QuickRecorder/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "icon_16x16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "icon_16x16@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "icon_32x32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "icon_32x32@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "icon_128x128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "icon_128x128@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "icon_256x256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "icon_256x256@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "icon_512x512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "icon_512x512@2x.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/black_white.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0xFF", "green" : "0xFF", "red" : "0xFF" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0x00", "green" : "0x00", "red" : "0x00" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/buttonRed.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0x5E", "green" : "0x6A", "red" : "0xEC" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/buttonRedDark.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0x2B", "green" : "0x3A", "red" : "0xCD" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/dark_my_red.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0x0C", "green" : "0x1B", "red" : "0xAA" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0x5D", "green" : "0x6A", "red" : "0xEC" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/myblue.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xE1", "green" : "0xA7", "red" : "0x3D" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0xF6", "green" : "0xBB", "red" : "0x51" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/mygreen.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x37", "green" : "0xC3", "red" : "0x21" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "srgb", "components" : { "alpha" : "1.000", "blue" : "0x55", "green" : "0xE1", "red" : "0x3D" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Colors/mypurple.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0xCF", "green" : "0x56", "red" : "0x57" } }, "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0xEA", "green" : "0x61", "red" : "0x62" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/audioIcon.imageset/Contents.json ================================================ { "images" : [ { "filename" : "MissingArtwork_Music_onLight_55A54008AD1BA589AA210D2629C1DF41_0.png", "idiom" : "universal" }, { "appearances" : [ { "appearance" : "luminosity", "value" : "dark" } ], "filename" : "MissingArtwork_Music_onDark_55A54008AD1BA589AA210D2629C1DF41_0.png", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/camera.imageset/Contents.json ================================================ { "images" : [ { "filename" : "menubar@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "menubar@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/save.imageset/Contents.json ================================================ { "images" : [ { "filename" : "save.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "save@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/unknowScreen.imageset/Contents.json ================================================ { "images" : [ { "filename" : "unknow screen.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "unknow screen@2x.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "template-rendering-intent" : "template" } } ================================================ FILE: QuickRecorder/Assets.xcassets/Others/window.select.imageset/Contents.json ================================================ { "images" : [ { "filename" : "macwindow.and.cursorarrow.svg", "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 }, "properties" : { "preserves-vector-representation" : true, "template-rendering-intent" : "template" } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/blacklist.imageset/Contents.json ================================================ { "images" : [ { "filename" : "blacklist@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "blacklist.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/film.imageset/Contents.json ================================================ { "images" : [ { "filename" : "film@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "film.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/gear.imageset/Contents.json ================================================ { "images" : [ { "filename" : "gear@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "gear.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/hotkey.imageset/Contents.json ================================================ { "images" : [ { "filename" : "hotkey@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "hotkey.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Settings/record.imageset/Contents.json ================================================ { "images" : [ { "filename" : "record@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "record.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/fuzi1.imageset/Contents.json ================================================ { "images" : [ { "filename" : "fuzi@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "fuzi.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/fuzi2.imageset/Contents.json ================================================ { "images" : [ { "filename" : "fuziBlur@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "fuziBlur.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/fuzi3.imageset/Contents.json ================================================ { "images" : [ { "filename" : "fuziBlur2@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "fuziBlur2.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/hongbao1.imageset/Contents.json ================================================ { "images" : [ { "filename" : "hongbao@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "hongbao.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/hongbao2.imageset/Contents.json ================================================ { "images" : [ { "filename" : "hongbaoBlur@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "hongbaoBlur.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/ChineseNewYear/hongbao3.imageset/Contents.json ================================================ { "images" : [ { "filename" : "hongbaoBlur2@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "hongbaoBlur2.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/christmasTree1.imageset/Contents.json ================================================ { "images" : [ { "filename" : "tree@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "tree.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/christmasTree2.imageset/Contents.json ================================================ { "images" : [ { "filename" : "treeBlur@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "treeBlur.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/snowflake1.imageset/Contents.json ================================================ { "images" : [ { "filename" : "snow@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "snow.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/snowflake2.imageset/Contents.json ================================================ { "images" : [ { "filename" : "snowBlur@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "snowBlur.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Christmas/snowflake3.imageset/Contents.json ================================================ { "images" : [ { "filename" : "snowBlur2@1x.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "snowBlur2.png", "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Assets.xcassets/Surprise/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/Base.lproj/Credits.rtf ================================================ {\rtf1\ansi\ansicpg936\cocoartf2761 \cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset134 PingFangSC-Semibold;\f1\fnil\fcharset134 PingFangSC-Regular;} {\colortbl;\red255\green255\blue255;} {\*\expandedcolortbl;;} \paperw12240\paperh15840\margl1440\margr1440\vieww9000\viewh8400\viewkind0 \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 \f0\b\fs24 \cf0 Lightweight screen recorder based on ScreenCapture Kit\ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 \f1\b0\fs10 \cf0 \ \pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\partightenfactor0 \fs20 \cf0 View source code on {\field{\*\fldinst{HYPERLINK "https://github.com/lihaoyun6/QuickRecorder"}}{\fldrslt Github}}} ================================================ FILE: QuickRecorder/Info.plist ================================================ CFBundleDocumentTypes CFBundleTypeExtensions mov mp4 CFBundleTypeRole Viewer LSHandlerRank Alternate NSDocumentClass NSObject NSIsInformational CFBundleTypeExtensions qma CFBundleTypeIconFile qmaIcon CFBundleTypeName QuickRecorder Multi-Track Audio CFBundleTypeRole Viewer LSHandlerRank Owner LSItemContentTypes com.lihaoyun6.QuickRecorder.qma LSTypeIsPackage NSAppleScriptEnabled OSAScriptingDefinition Scriptable SUFeedURL https://raw.githubusercontent.com/lihaoyun6/QuickRecorder/main/appcast.xml SUPublicEDKey dP6zY7RAyK3DtV/dDrQ2V2dB0BY36jADwMKHZIX89u4= UTExportedTypeDeclarations UTTypeConformsTo com.apple.package UTTypeDescription QuickRecorder Multi-Track Audio UTTypeIcons UTTypeIconBadgeName qmaIcon UTTypeIdentifier com.lihaoyun6.QuickRecorder.qma UTTypeTagSpecification public.filename-extension qma public.mime-type application/x-qma ================================================ FILE: QuickRecorder/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: QuickRecorder/QuickRecorder.entitlements ================================================ com.apple.security.device.audio-input com.apple.security.device.camera ================================================ FILE: QuickRecorder/QuickRecorderApp.swift ================================================ // // QuickRecorderApp.swift // QuickRecorder // // Created by apple on 2024/4/16. // import AppKit import SwiftUI import AVFAudio import AVFoundation import ScreenCaptureKit import UserNotifications import KeyboardShortcuts import ServiceManagement import CoreMediaIO import Sparkle let isMacOS12 = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 12 let isMacOS14 = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 14 let isMacOS15 = ProcessInfo.processInfo.operatingSystemVersion.majorVersion == 15 var scPerm = false let fd = FileManager.default let ud = UserDefaults.standard var statusBarItem: NSStatusItem! var mouseMonitor: Any? var keyMonitor: Any? var hideMousePointer = false var hideScreenMagnifier = false let updateTimer = Timer.publish(every: 0.5, on: .main, in: .common).autoconnect() let mousePointer = NSWindow(contentRect: NSRect(x: -70, y: -70, width: 70, height: 70), styleMask: [.borderless], backing: .buffered, defer: false) let screenMagnifier = NSWindow(contentRect: NSRect(x: -402, y: -402, width: 402, height: 348), styleMask: [.borderless], backing: .buffered, defer: false) let camWindow = NSPanel(contentRect: NSRect(x: 200, y: 200, width: 200, height: 200), styleMask: [.fullSizeContentView, .resizable, .nonactivatingPanel], backing: .buffered, defer: false) let deviceWindow = NSWindow(contentRect: NSRect(x: 200, y: 200, width: 200, height: 200), styleMask: [.fullSizeContentView, .resizable], backing: .buffered, defer: false) let controlPanel = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 10, height: 10), styleMask: [.fullSizeContentView], backing: .buffered, defer: false) let countdownPanel = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 120, height: 120), styleMask: [.fullSizeContentView], backing: .buffered, defer: false) let previewWindow = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 266, height: 156), styleMask: [.fullSizeContentView], backing: .buffered, defer: false) var updaterController: SPUStandardUpdaterController! @main struct QuickRecorderApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate //private let updaterController: SPUStandardUpdaterController init() { // If you want to start the updater manually, pass false to startingUpdater and call .startUpdater() later // This is where you can also pass an updater delegate if you need one updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) } var body: some Scene { DocumentGroup(newDocument: qmaPackageHandle()) { file in //if SCContext.stream == nil { if let fileURL = file.fileURL { qmaPlayerView(document: file.$document, fileURL: fileURL) .frame(minWidth: 400, minHeight: 100, maxHeight: 100) .focusable(false) } //} } .myWindowIsContentResizable() .commands { SidebarCommands() CommandGroup(replacing: .saveItem) {} CommandGroup(replacing: .newItem) {} CommandGroup(replacing: .textEditing) {} } Settings { SettingsView() .background( WindowAccessor( onWindowOpen: { w in if let w = w { //w.level = .floating w.titlebarSeparatorStyle = .none guard let nsSplitView = findNSSplitVIew(view: w.contentView), let controller = nsSplitView.delegate as? NSSplitViewController else { return } controller.splitViewItems.first?.canCollapse = false controller.splitViewItems.first?.minimumThickness = 140 controller.splitViewItems.first?.maximumThickness = 140 w.orderFront(nil) } }) ) } .handlesExternalEvents(matching: []) .commands { CommandGroup(replacing: .newItem) {} CommandGroup(after: .appInfo) { CheckForUpdatesView(updater: updaterController.updater) } } } } extension Scene { func myWindowIsContentResizable() -> some Scene { if #available(macOS 13.0, *) { return self.windowResizability(.contentSize) } else { return self } } } class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOutput, AVCaptureVideoDataOutputSampleBufferDelegate { static let shared = AppDelegate() var filter: SCContentFilter? var isCameraReady = false var isPresenterON = false var isResizing = false var presenterType = "OFF" var frameQueue = FixedLengthArray(maxLength: 20) @AppStorage("showOnDock") var showOnDock: Bool = true @AppStorage("showMenubar") var showMenubar: Bool = false @AppStorage("enableAEC") var enableAEC: Bool = false @AppStorage("recordMic") var recordMic: Bool = false @AppStorage("micDevice") var micDevice: String = "default" @AppStorage("remuxAudio") var remuxAudio: Bool = true @AppStorage("recordWinSound") var recordWinSound: Bool = true @AppStorage("recordHDR") var recordHDR: Bool = false @AppStorage("encoder") var encoder: Encoder = .h265 @AppStorage("highRes") var highRes: Int = 2 @AppStorage("AECLevel") var AECLevel: String = "mid" @AppStorage("withAlpha") var withAlpha: Bool = false @AppStorage("saveDirectory") var saveDirectory: String? @AppStorage("countdown") var countdown: Int = 0 @AppStorage("poSafeDelay") var poSafeDelay: Int = 1 @AppStorage("highlightMouse") var highlightMouse: Bool = false @AppStorage("includeMenuBar") var includeMenuBar: Bool = true @AppStorage("hideDesktopFiles") var hideDesktopFiles: Bool = false @AppStorage("trimAfterRecord") var trimAfterRecord: Bool = false @AppStorage("miniStatusBar") var miniStatusBar: Bool = false @AppStorage("hideSelf") var hideSelf: Bool = true @AppStorage("preventSleep") var preventSleep: Bool = true @AppStorage("showPreview") var showPreview: Bool = true @AppStorage("background") var background: BackgroundType = .wallpaper @AppStorage("showMouse") var showMouse: Bool = true @AppStorage("frameRate") var frameRate: Int = 60 @AppStorage("videoQuality") var videoQuality: Double = 1.0 @AppStorage("videoFormat") var videoFormat: VideoFormat = .mp4 @AppStorage("audioFormat") var audioFormat: AudioFormat = .aac @AppStorage("audioQuality") var audioQuality: AudioQuality = .high @AppStorage("pixelFormat") var pixelFormat: PixFormat = .delault @AppStorage("hideCCenter") var hideCCenter: Bool = false func mousePointerReLocation(event: NSEvent) { if event.type == .scrollWheel { return } if !highlightMouse || hideMousePointer || SCContext.stream == nil || SCContext.streamType == .window { mousePointer.orderOut(nil) return } let mouseLocation = event.locationInWindow var windowFrame = mousePointer.frame windowFrame.origin = NSPoint(x: mouseLocation.x - windowFrame.width / 2, y: mouseLocation.y - windowFrame.height / 2) mousePointer.contentView = NSHostingView(rootView: MousePointerView(event: event)) mousePointer.setFrameOrigin(windowFrame.origin) mousePointer.orderFront(nil) } func screenMagnifierReLocation(event: NSEvent) { if !SCContext.isMagnifierEnabled || hideScreenMagnifier { screenMagnifier.orderOut(nil); return } let mouseLocation = event.locationInWindow var windowFrame = screenMagnifier.frame windowFrame.origin = NSPoint(x: mouseLocation.x - windowFrame.width / 2, y: mouseLocation.y - windowFrame.height / 2) guard let image = NSImage.createScreenShot() else { return } let rect = NSRect(x: mouseLocation.x - 67, y: mouseLocation.y - 58, width: 134, height: 116) let croppedImage = image.trim(rect: rect) screenMagnifier.contentView = NSHostingView(rootView: ScreenMagnifier(screenShot: croppedImage, event: event)) screenMagnifier.setFrameOrigin(windowFrame.origin) screenMagnifier.orderFront(nil) } func registerGlobalMouseMonitor() { mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.scrollWheel, .mouseMoved, .rightMouseUp, .rightMouseDown, .rightMouseDragged, .leftMouseUp, .leftMouseDown, .leftMouseDragged, .otherMouseUp, .otherMouseDown, .otherMouseDragged]) { event in self.mousePointerReLocation(event: event) self.screenMagnifierReLocation(event: event) } } func stopGlobalMouseMonitor() { mousePointer.orderOut(nil) if let monitor = mouseMonitor { NSEvent.removeMonitor(monitor); mouseMonitor = nil } } func applicationWillTerminate(_ aNotification: Notification) { if SCContext.stream != nil { SCContext.stopRecording() } } func application(_ application: NSApplication, open urls: [URL]) { for url in urls { if SCContext.trimingList.contains(url) { continue } createNewWindow(view: VideoTrimmerView(videoURL: url), title: url.lastPathComponent, random: true, only: false) closeMainWindow() } } func applicationWillFinishLaunching(_ notification: Notification) { scPerm = SCContext.updateAvailableContentSync() != nil let process = NSWorkspace.shared.runningApplications.filter({ $0.bundleIdentifier == "com.lihaoyun6.QuickRecorder" }) if process.count > 1 { DispatchQueue.main.async { let button = createAlert(title: "QuickRecorder is Running".local, message: "Please do not run multiple instances!".local, button1: "Quit".local).runModal() if button == .alertFirstButtonReturn { NSApp.terminate(self) } } } lazy var userDesktop = (NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true) as [String]).first! ud.register( // default defaults (used if not set) defaults: [ "audioFormat": AudioFormat.aac.rawValue, "audioQuality": AudioQuality.high.rawValue, "background": BackgroundType.wallpaper.rawValue, "frameRate": 60, "highRes": 2, "hideSelf": true, "highlightMouse" : false, "hideDesktopFiles": false, "includeMenuBar": true, "videoQuality": 1.0, "countdown": 0, "videoFormat": VideoFormat.mp4.rawValue, "pixelFormat": PixFormat.delault.rawValue, "encoder": Encoder.h264.rawValue, "poSafeDelay": 1, "saveDirectory": userDesktop as NSString, "showMouse": true, "recordMic": false, "remuxAudio": isMacOS12 ? false : true, "recordWinSound": isMacOS12 ? false : true, "trimAfterRecord": false, "showOnDock": true, "showMenubar": false, "enableAEC": false, "recordHDR": false, "preventSleep": true, "showPreview": isMacOS12 ? false : true, "savedArea": [String: [String: CGFloat]]() ] ) if highRes == 0 { highRes = 2 } if showOnDock { NSApp.setActivationPolicy(.regular) } if isMacOS12 { showPreview = false; remuxAudio = false } UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in if let error = error { print("Notification authorization denied: \(error.localizedDescription)") } } var allow : UInt32 = 1 let dataSize : UInt32 = 4 let zero : UInt32 = 0 var prop = CMIOObjectPropertyAddress( mSelector: CMIOObjectPropertySelector(kCMIOHardwarePropertyAllowScreenCaptureDevices), mScope: CMIOObjectPropertyScope(kCMIOObjectPropertyScopeGlobal), mElement: CMIOObjectPropertyElement(kCMIOObjectPropertyElementMain)) CMIOObjectSetPropertyData(CMIOObjectID(kCMIOObjectSystemObject), &prop, zero, nil, dataSize, &allow) statusBarItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) statusBarItem.button?.image = NSImage() mousePointer.title = "Mouse Pointer".local mousePointer.level = .screenSaver mousePointer.ignoresMouseEvents = true mousePointer.isReleasedWhenClosed = false mousePointer.backgroundColor = NSColor.clear screenMagnifier.title = "Screen Magnifier".local screenMagnifier.level = .floating screenMagnifier.ignoresMouseEvents = true screenMagnifier.isReleasedWhenClosed = false screenMagnifier.backgroundColor = NSColor.clear camWindow.title = "Camera Overlayer".local camWindow.level = .floating camWindow.isReleasedWhenClosed = false camWindow.isMovableByWindowBackground = true camWindow.backgroundColor = NSColor.clear camWindow.collectionBehavior = [.canJoinAllSpaces] countdownPanel.title = "Countdown Panel".local countdownPanel.level = .floating countdownPanel.isReleasedWhenClosed = false countdownPanel.isMovableByWindowBackground = false countdownPanel.backgroundColor = NSColor.clear deviceWindow.title = "iDevice Overlayer".local deviceWindow.level = .floating deviceWindow.isReleasedWhenClosed = false deviceWindow.isMovableByWindowBackground = true deviceWindow.backgroundColor = NSColor.clear controlPanel.title = "Recording Controller".local controlPanel.level = .floating controlPanel.titleVisibility = .hidden controlPanel.backgroundColor = NSColor.clear controlPanel.isReleasedWhenClosed = false controlPanel.titlebarAppearsTransparent = true controlPanel.isMovableByWindowBackground = true previewWindow.level = .statusBar previewWindow.titlebarAppearsTransparent = true previewWindow.titleVisibility = .hidden previewWindow.isReleasedWhenClosed = false previewWindow.backgroundColor = .clear KeyboardShortcuts.onKeyDown(for: .showPanel) { _ = self.applicationShouldHandleReopen(NSApp, hasVisibleWindows: true) if SCContext.stream == nil { NSApp.activate(ignoringOtherApps: true) } } KeyboardShortcuts.onKeyDown(for: .saveFrame) { if SCContext.stream != nil { SCContext.saveFrame = true }} KeyboardShortcuts.onKeyDown(for: .screenMagnifier) { if SCContext.stream != nil { SCContext.isMagnifierEnabled.toggle() }} KeyboardShortcuts.onKeyDown(for: .stop) { if SCContext.stream != nil { SCContext.stopRecording() }} KeyboardShortcuts.onKeyDown(for: .pauseResume) { if SCContext.stream != nil { SCContext.pauseRecording() }} KeyboardShortcuts.onKeyDown(for: .startWithAudio) {[self] in if SCContext.streamType != nil { return } closeAllWindow() prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil, fastStart: true) } KeyboardShortcuts.onKeyDown(for: .startWithScreen) {[self] in if SCContext.stream != nil { return } closeAllWindow() prepRecord(type: "display", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil, fastStart: true) } KeyboardShortcuts.onKeyDown(for: .startWithArea) {[self] in if SCContext.stream != nil { return } closeAllWindow() showAreaSelector(size: NSSize(width: 600, height: 450)) } KeyboardShortcuts.onKeyDown(for: .startWithWindow) { [self] in if SCContext.stream != nil { return } closeAllWindow() let frontmostApp = NSWorkspace.shared.frontmostApplication if let pid = frontmostApp?.processIdentifier { guard let scWindow = SCContext.getWindows().first(where: { $0.owningApplication?.processID == pid && $0.title != "" && $0.isOnScreen }) else { return } prepRecord(type: "window", screens: SCContext.getSCDisplayWithMouse(), windows: [scWindow], applications: nil, fastStart: true) return } } updateStatusBar() } func applicationDidFinishLaunching(_ aNotification: Notification) { closeAllWindow() if showOnDock { _ = applicationShouldHandleReopen(NSApp, hasVisibleWindows: true) } tips("Would you like to use H.265 format for better video quality and smaller file size?", id: "qr.switch-to-h265.note", buttonTitle: "Use H.265", switchButton: true) { ud.setValue(Encoder.h265.rawValue, forKey: "encoder") } } func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { if SCContext.stream == nil { let w1 = NSApp.windows.filter({ !$0.title.contains("Item-0") && !$0.title.isEmpty && $0.isVisible }) let w2 = w1.filter({ !$0.title.contains(".qma") }) if (!w1.isEmpty && w2.isEmpty) || w1.isEmpty { let offset = (!showOnDock && !showMenubar) ? 127 : 0 let width = isMacOS12 ? 800 : 928 let mainPanel = EscPanel(contentRect: NSRect(x: 0, y: 0, width: width + offset, height: 100), styleMask: [.fullSizeContentView, .nonactivatingPanel], backing: .buffered, defer: false) mainPanel.contentView = NSHostingView(rootView: ContentView()) mainPanel.title = "QuickRecorder".local mainPanel.isOpaque = false mainPanel.level = .floating mainPanel.isRestorable = false mainPanel.backgroundColor = .clear mainPanel.isReleasedWhenClosed = false mainPanel.isMovableByWindowBackground = true mainPanel.collectionBehavior = [.canJoinAllSpaces] mainPanel.center() if let screen = mainPanel.screen { let wX = (screen.frame.width - mainPanel.frame.width) / 2 + screen.frame.minX let wY = (screen.frame.height - mainPanel.frame.height) / 2 + screen.frame.minY mainPanel.setFrameOrigin(NSPoint(x: wX, y: wY)) } mainPanel.makeKeyAndOrderFront(self) if #unavailable(macOS 13) { NSApp.activate(ignoringOtherApps: true) } PopoverState.shared.isShowing = false } } return false } func openSettingPanel() { NSApp.activate(ignoringOtherApps: true) if #available(macOS 14, *) { NSApp.mainMenu?.items.first?.submenu?.item(at: 3)?.performAction() } else if #available(macOS 13, *) { NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil) } else { NSApp.sendAction(Selector(("showPreferencesWindow:")), to: nil, from: nil) } } class EscPanel: NSPanel { override func cancelOperation(_ sender: Any?) { self.close() } override var canBecomeKey: Bool { return true } } } func closeMainWindow() { for w in NSApp.windows.filter({ $0.title == "QuickRecorder".local }) { w.close() } } func closeAllWindow(except: String = "") { for w in NSApp.windows.filter({ $0.title != "Item-0" && $0.title != "" && !$0.title.lowercased().contains(".qma") && !$0.title.contains(except) }) { w.close() } } func findNSSplitVIew(view: NSView?) -> NSSplitView? { var queue = [NSView]() if let root = view { queue.append(root) } while !queue.isEmpty { let current = queue.removeFirst() if current is NSSplitView { return current as? NSSplitView } for subview in current.subviews { queue.append(subview) } } return nil } func getStatusBarWidth() -> CGFloat { @AppStorage("miniStatusBar") var miniStatusBar: Bool = false var width = 158.0 switch SCContext.streamType { case nil: width = miniStatusBar ? 36.0 : 36.0 case .idevice: width = miniStatusBar ? 68.0 : 138.0 case .systemaudio: width = miniStatusBar ? 68.0 : 114.0 default: width = miniStatusBar ? 78.0 : 158.0 } return width } func process(path: String, arguments: [String]) -> String? { let task = Process() task.launchPath = path task.arguments = arguments task.standardError = Pipe() let outputPipe = Pipe() defer { outputPipe.fileHandleForReading.closeFile() } task.standardOutput = outputPipe do { try task.run() } catch let error { print("\(error.localizedDescription)") return nil } let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() let output = String(decoding: outputData, as: UTF8.self) if output.isEmpty { return nil } return output.trimmingCharacters(in: .newlines) } func tips(_ message: String, title: String? = nil, id: String, buttonTitle: String = "OK", switchButton: Bool = false, width: Int? = nil, action: (() -> Void)? = nil) { let never = (ud.object(forKey: "neverRemindMe") as? [String]) ?? [] if !never.contains(id) { if switchButton { let alert = createAlert(title: title ?? Bundle.main.appName + " Tips".local, message: message, button1: buttonTitle, button2: "Don't remind me again", width: width).runModal() if alert == .alertSecondButtonReturn { ud.setValue(never + [id], forKey: "neverRemindMe") } if alert == .alertFirstButtonReturn { action?() } } else { let alert = createAlert(title: title ?? Bundle.main.appName + " Tips".local, message: message, button1: "Don't remind me again", button2: buttonTitle, width: width).runModal() if alert == .alertFirstButtonReturn { ud.setValue(never + [id], forKey: "neverRemindMe") } if alert == .alertSecondButtonReturn { action?() } } } } func createAlert(level: NSAlert.Style = .warning, title: String, message: String, button1: String, button2: String = "", width: Int? = nil) -> NSAlert { let alert = NSAlert() alert.messageText = title.local alert.informativeText = message.local alert.addButton(withTitle: button1.local) if button2 != "" { alert.addButton(withTitle: button2.local) } alert.alertStyle = level if let width = width { alert.accessoryView = NSView(frame: NSMakeRect(0, 0, Double(width), 0)) } return alert } func showAlertSyncOnMainThread(level: NSAlert.Style = .warning, title: String, message: String, button1: String, button2: String = "", width: Int? = nil) -> NSApplication.ModalResponse { var response: NSApplication.ModalResponse = .abort let semaphore = DispatchSemaphore(value: 0) DispatchQueue.main.async { let alert = createAlert(level: level, title: title, message: message, button1: button1, button2: button2, width: width) response = alert.runModal() semaphore.signal() } semaphore.wait() return response } extension Bundle { var appName: String { let appName = self.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String ?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String ?? "Unknown App Name" return appName } } extension String { var local: String { return NSLocalizedString(self, comment: "") } var deletingPathExtension: String { return (self as NSString).deletingPathExtension } var pathExtension: String { return (self as NSString).pathExtension } var lastPathComponent: String { return (self as NSString).lastPathComponent } var url: URL { return URL(fileURLWithPath: self) } } extension NSMenuItem { func performAction() { guard let menu else { return } menu.performActionForItem(at: menu.index(of: self)) } } extension NSImage { static func createScreenShot() -> NSImage? { let excludedAppBundleIDs = ["com.lihaoyun6.QuickRecorder"] var exclusionPIDs = [Int]() for app in NSWorkspace.shared.runningApplications { if excludedAppBundleIDs.contains(app.bundleIdentifier ?? "") { exclusionPIDs.append(Int(app.processIdentifier)) } } let windowDescriptions = CGWindowListCopyWindowInfo(.optionOnScreenOnly, kCGNullWindowID) as? [[String: Any]] ?? [] var windowIDs = [CGWindowID]() for windowDict in windowDescriptions { if let windowProcessID = windowDict[kCGWindowOwnerPID as String] as? Int, !exclusionPIDs.contains(windowProcessID), let windowID = windowDict[kCGWindowNumber as String] as? CGWindowID { windowIDs.append(windowID) } } let pointer = UnsafeMutablePointer.allocate(capacity: windowIDs.count) for (index, window) in windowIDs.enumerated() { pointer[index] = UnsafeRawPointer(bitPattern: UInt(window)) } let cWindowIDArray: CFArray = CFArrayCreate(kCFAllocatorDefault, pointer, windowIDs.count, nil) guard let imageRef = CGImage(windowListFromArrayScreenBounds: CGRect.infinite, windowArray: cWindowIDArray, imageOption: []) else { print("No image available") return nil } let factor = SCContext.getScreenWithMouse()?.backingScaleFactor ?? 1.0 return NSImage(cgImage: imageRef, size: NSSize(width: CGFloat(imageRef.width)/factor, height: CGFloat(imageRef.height)/factor)) } func saveToFile(_ url: URL, type: NSBitmapImageRep.FileType = .png) { if let tiffData = self.tiffRepresentation, let imageRep = NSBitmapImageRep(data: tiffData) { let pngData = imageRep.representation(using: type, properties: [:]) do { try pngData?.write(to: url) } catch { print("Error saving image: \(error.localizedDescription)") } } } func trim(rect: CGRect) -> NSImage { let result = NSImage(size: rect.size) result.lockFocus() let destRect = CGRect(origin: .zero, size: result.size) self.draw(in: destRect, from: rect, operation: .copy, fraction: 1.0) result.unlockFocus() return result } } class NNSWindow: NSWindow { override var canBecomeKey: Bool { return true } } struct FixedLengthArray { private var array: [T] = [] private let maxLength: Int init(maxLength: Int) { self.maxLength = maxLength } mutating func append(_ element: T) { if array.count >= maxLength { array.removeFirst() } array.append(element) } func getArray() -> [T] { return array } } extension utsname { static var sMachine: String { var utsname = utsname() uname(&utsname) return withUnsafePointer(to: &utsname.machine) { $0.withMemoryRebound(to: CChar.self, capacity: Int(_SYS_NAMELEN)) { String(cString: $0) } } } static var isAppleSilicon: Bool { sMachine == "arm64" } } enum AudioQuality: Int { case normal = 128, good = 192, high = 256, extreme = 320 } enum AudioFormat: String { case aac, alac, flac, opus, mp3 } enum VideoFormat: String { case mov, mp4 } enum PixFormat: String { case delault, yuv420p8v, yuv420p8f, yuv420p10v, yuv420p10f, bgra32 } enum ColSpace: String { case delault, srgb, p3, bt709, bt2020 } enum Encoder: String { case h264, h265 } enum StreamType: Int { case screen, window, windows, application, screenarea, systemaudio, idevice, camera } enum BackgroundType: String { case wallpaper, clear, black, white, red, green, yellow, orange, gray, blue, custom } ================================================ FILE: QuickRecorder/RecordEngine.swift ================================================ // // RecordEngine.swift // QuickRecorder // // Created by apple on 2024/4/17. // import Foundation import UserNotifications import ScreenCaptureKit import AVFoundation import AVFAudio import VideoToolbox import AECAudioStream extension AppDelegate { @objc func prepRecord(type: String, screens: SCDisplay?, windows: [SCWindow]?, applications: [SCRunningApplication]?, fastStart: Bool = false) { switch type { case "window": SCContext.streamType = .window case "windows": SCContext.streamType = .windows case "display": SCContext.streamType = .screen case "application": SCContext.streamType = .application case "area": SCContext.streamType = .screenarea case "audio": SCContext.streamType = .systemaudio default: return // if we don't even know what to record I don't think we should even try } var isDirectory: ObjCBool = false let outputPath = saveDirectory! if fd.fileExists(atPath: outputPath, isDirectory: &isDirectory) { if !isDirectory.boolValue { SCContext.streamType = nil _ = createAlert(title: "Failed to Record".local, message: "The output path is a file instead of a folder!".local, button1: "OK").runModal() return } } else { do { try fd.createDirectory(atPath: outputPath, withIntermediateDirectories: true, attributes: nil) } catch { SCContext.streamType = nil _ = createAlert(title: "Failed to Record".local, message: "Unable to create output folder!".local, button1: "OK").runModal() return } } // file preparation if let screens = screens { SCContext.screen = SCContext.availableContent!.displays.first(where: { $0 == screens }) } else { SCContext.streamType = nil; return } if let windows = windows { SCContext.window = SCContext.availableContent!.windows.filter({ windows.contains($0) }) } else { if SCContext.streamType == .window { SCContext.streamType = nil; return } } if let applications = applications { SCContext.application = SCContext.availableContent!.applications.filter({ applications.contains($0) }) } else { if SCContext.streamType == .application { SCContext.streamType = nil; return } } let screen = SCContext.screen ?? SCContext.getSCDisplayWithMouse()! let qrSelf = SCContext.getSelf() let qrWindows = SCContext.getSelfWindows() let dockApp = SCContext.availableContent!.applications.first(where: { $0.bundleIdentifier.description == "com.apple.dock" }) let wallpaper = SCContext.availableContent!.windows.filter({ guard let title = $0.title else { return false } return $0.owningApplication?.bundleIdentifier == "com.apple.dock" && title != "LPSpringboard" && title != "Dock" }) let desktop = SCContext.availableContent!.windows.filter({ guard let title = $0.title else { return false } return $0.owningApplication?.bundleIdentifier == "" && title == "Desktop" }) let dockWindow = SCContext.availableContent!.windows.filter({ guard let title = $0.title else { return true } return $0.owningApplication?.bundleIdentifier == "com.apple.dock" && title == "Dock" }) let desktopFiles = SCContext.availableContent!.windows.filter({ $0.owningApplication?.bundleIdentifier == "com.apple.finder" && $0.title == "" && $0.frame == screen.frame }) let controlCenterWindow = SCContext.availableContent!.applications.filter({ $0.bundleIdentifier == "com.apple.controlcenter" }) let mouseWindow = SCContext.availableContent!.windows.filter({ $0.title == "Mouse Pointer".local && $0.owningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier }) let camLayer = SCContext.availableContent!.windows.filter({ $0.title == "Camera Overlayer".local && $0.owningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier }) var appBlackList = [String]() if let savedData = ud.data(forKey: "hiddenApps"), let decodedApps = try? JSONDecoder().decode([AppInfo].self, from: savedData) { appBlackList = (decodedApps as [AppInfo]).map({ $0.bundleID }) } let excliudedApps = SCContext.availableContent!.applications.filter({ appBlackList.contains($0.bundleIdentifier) }) if SCContext.streamType == .window || SCContext.streamType == .windows { if var includ = SCContext.window { if includ.count > 1 { if highlightMouse { includ += mouseWindow } if background.rawValue == BackgroundType.wallpaper.rawValue { if dockApp != nil { includ += wallpaper }} SCContext.filter = SCContentFilter(display: screen, including: includ + camLayer) if #available(macOS 14.2, *) { SCContext.filter?.includeMenuBar = includeMenuBar } } else { SCContext.streamType = .window SCContext.filter = SCContentFilter(desktopIndependentWindow: includ[0]) } } } else { if SCContext.streamType == .screen || SCContext.streamType == .screenarea { if SCContext.streamType == .screenarea { if let area = SCContext.screenArea, let name = screen.nsScreen?.localizedName { let a = ["x": area.origin.x, "y": area.origin.y, "width": area.width, "height": area.height] ud.set([name: a], forKey: "savedArea") } } var excluded = [SCRunningApplication]() var except = [SCWindow]() excluded += excliudedApps if hideCCenter { excluded += controlCenterWindow } if hideSelf { if let qrWindows = qrWindows { except += qrWindows }} if background.rawValue != BackgroundType.wallpaper.rawValue { if dockApp != nil { except += wallpaper except += desktop }} if hideDesktopFiles { except += desktopFiles } SCContext.filter = SCContentFilter(display: screen, excludingApplications: excluded, exceptingWindows: except) if #available(macOS 14.2, *) { SCContext.filter?.includeMenuBar = ((SCContext.streamType == .screen || SCContext.streamType == .screenarea) && includeMenuBar) } } if SCContext.streamType == .application { var includ = SCContext.application! var except = [SCWindow]() if let qrSelf = qrSelf { includ.append(qrSelf) } let withFinder = includ.map{ $0.bundleIdentifier }.contains("com.apple.finder") if withFinder && hideDesktopFiles { except += desktopFiles } if hideSelf { if let qrWindows = qrWindows { except += qrWindows }} //if ud.bool(forKey: "highlightMouse") { if let qrSelf = qrSelf { includ.append(qrSelf) }} if background.rawValue == BackgroundType.wallpaper.rawValue { if let dock = dockApp { includ.append(dock); except += dockWindow}} SCContext.filter = SCContentFilter(display: screen, including: includ, exceptingWindows: except) if #available(macOS 14.2, *) { SCContext.filter?.includeMenuBar = includeMenuBar } } } if SCContext.streamType == .systemaudio { SCContext.filter = SCContentFilter(display: screen, excludingApplications: [], exceptingWindows: []) prepareAudioRecording() } Task { await record(filter: SCContext.filter!, fastStart: fastStart) } } func record(filter: SCContentFilter, fastStart: Bool = true) async { SCContext.timeOffset = CMTimeMake(value: 0, timescale: 0) SCContext.isPaused = false SCContext.isResume = false let audioOnly = SCContext.streamType == .systemaudio let conf: SCStreamConfiguration #if compiler(>=6.0) if recordHDR { if #available(macOS 15, *) { // TODO change here. https://developer.apple.com/videos/play/wwdc2024/10088/?time=191 // For canonical display, it means you are capturing HDR content that is optimized for sharing with other HDR devices. // hdrLocalDisplay or hdrCanonicalDisplay conf = SCStreamConfiguration(preset: .captureHDRStreamLocalDisplay) } else { conf = SCStreamConfiguration() } } else { conf = SCStreamConfiguration() } #else conf = SCStreamConfiguration() #endif conf.width = 2 conf.height = 2 if !audioOnly { if #available(macOS 14.0, *) { conf.width = Int(filter.contentRect.width) * (highRes == 2 ? Int(filter.pointPixelScale) : 1) conf.height = Int(filter.contentRect.height) * (highRes == 2 ? Int(filter.pointPixelScale) : 1) } else { guard let pointPixelScaleOld = (SCContext.screen ?? SCContext.getSCDisplayWithMouse()!).nsScreen?.backingScaleFactor else { return } if SCContext.streamType == .application || SCContext.streamType == .windows || SCContext.streamType == .screen { let frame = (SCContext.screen ?? SCContext.getSCDisplayWithMouse()!).frame conf.width = Int(frame.width) conf.height = Int(frame.height) } if SCContext.streamType == .window { let frame = SCContext.window![0].frame conf.width = Int(frame.width) conf.height = Int(frame.height) } if SCContext.streamType == .screenarea { let frame = SCContext.screenArea! conf.width = Int(frame.width) conf.height = Int(frame.height) } conf.width = conf.width * (highRes == 2 ? Int(pointPixelScaleOld) : 1) conf.height = conf.height * (highRes == 2 ? Int(pointPixelScaleOld) : 1) } if fastStart{ conf.showsCursor = false } else{ conf.showsCursor = showMouse } if background.rawValue != BackgroundType.wallpaper.rawValue { conf.backgroundColor = SCContext.getBackgroundColor() } if !recordHDR { conf.pixelFormat = kCVPixelFormatType_32BGRA conf.colorSpaceName = CGColorSpace.sRGB //if withAlpha { conf.pixelFormat = kCVPixelFormatType_32BGRA } } else { // For recording HDR in a BT2020 PQ container conf.colorSpaceName = CGColorSpace.itur_2100_PQ // https://developer.apple.com/videos/play/wwdc2022/10155/ guide on how to record 4k60 // streamConfiguration.pixelFormat = kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange // Note: 420 encoding causes color bleed at edges, e.g. youtube settings icon with red logo // conf.pixelFormat = kCVPixelFormatType_420YpCbCr10BiPlanarVideoRange // dont exceed 8 frames https://developer.apple.com/documentation/screencapturekit/scstreamconfiguration/queuedepth // lower queuedepth has more stutter, dont go below 4 https://github.com/nonstrict-hq/ScreenCaptureKit-Recording-example/blob/main/Sources/sckrecording/main.swift conf.queueDepth = 8 } } if #available(macOS 13, *) { conf.capturesAudio = recordWinSound || fastStart || audioOnly conf.sampleRate = 48000 conf.channelCount = 2 } // conf.minimumFrameInterval = CMTime(value: 1, timescale: audioOnly ? CMTimeScale.max : CMTimeScale(frameRate)) conf.minimumFrameInterval = CMTime(value: 1, timescale: audioOnly ? CMTimeScale.max : (frameRate >= 60 ? 0 : CMTimeScale(frameRate))) // CMTimeScale is the denominator in the fraction // conf.minimumFrameInterval = CMTime(seconds: audioOnly ? Double(CMTimeScale.max) : Double(1)/Double(frameRate), preferredTimescale: 10000) // note: ScreenCaptureKit only delivers frames when something changes // https://www.reddit.com/r/swift/comments/158n4c9/comment/ju847rm/?utm_source=share&utm_medium=web3x&utm_name=web3xcss&utm_term=1&utm_content=share_button //blog post from the reddit comment https://nonstrict.eu/blog/2023/recording-to-disk-with-screencapturekit/ //https://github.com/nonstrict-hq/ScreenCaptureKit-Recording-example // https://developer.apple.com/documentation/screencapturekit/scstreamconfiguration/minimumframeinterval //minimumFrameInterval: Use this value to throttle the rate at which you receive updates. The default value is 0, which indicates that the system uses the maximum supported frame rate. print("Frame interval passed to ScreenCaptureKit. (timescale is FPS. 0 means no throttling): \(conf.minimumFrameInterval)") if SCContext.streamType == .screenarea { if let nsRect = SCContext.screenArea { let newY = SCContext.screen!.frame.height - nsRect.size.height - nsRect.origin.y conf.sourceRect = CGRect(x: nsRect.origin.x, y: newY, width: nsRect.size.width, height: nsRect.size.height) if #available(macOS 14.0, *) { conf.width = Int(conf.sourceRect.width) * (highRes == 2 ? Int(filter.pointPixelScale) : 1) conf.height = Int(conf.sourceRect.height) * (highRes == 2 ? Int(filter.pointPixelScale) : 1) } else { guard let pointPixelScaleOld = (SCContext.screen ?? SCContext.getSCDisplayWithMouse()!).nsScreen?.backingScaleFactor else { return } conf.width = Int(conf.sourceRect.width) * (highRes == 2 ? Int(pointPixelScaleOld) : 1) conf.height = Int(conf.sourceRect.height) * (highRes == 2 ? Int(pointPixelScaleOld) : 1) } } } let encoderIsH265 = (encoder.rawValue == Encoder.h265.rawValue) || recordHDR if !audioOnly && !encoderIsH265 { var session: VTCompressionSession? let status = VTCompressionSessionCreate( allocator: nil, width: Int32(conf.width), height: Int32(conf.height), codecType: kCMVideoCodecType_H264, encoderSpecification: [kVTVideoEncoderSpecification_RequireHardwareAcceleratedVideoEncoder as String: true] as CFDictionary, imageBufferAttributes: nil, compressedDataAllocator: nil, outputCallback: nil, refcon: nil, compressionSessionOut: &session ) if status != noErr { let button = showAlertSyncOnMainThread( level: .critical, title: "Encoder Warning", message: "VideoToolbox H.264 hardware encoder doesn't support the current resolution.\nContinue with a software encoder will significantly increase the CPU usage.\n\nWould you like to use H.265 instead?".local, button1: "Use H.265", button2: "Continue with H.264" ) if button == .alertFirstButtonReturn { ud.setValue(Encoder.h265.rawValue, forKey: "encoder") } } } SCContext.stream = SCStream(filter: filter, configuration: conf, delegate: self) do { try SCContext.stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global()) if #available(macOS 13, *) { try SCContext.stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: .global()) } if !audioOnly { initVideo(conf: conf) } else { //SCContext.startTime = Date.now if recordMic { startMicRecording() } } try await SCContext.stream.startCapture() } catch { assertionFailure("capture failed".local) return } if !audioOnly { registerGlobalMouseMonitor() } DispatchQueue.main.async { updateStatusBar() } if preventSleep { SleepPreventer.shared.preventSleep(reason: "Screen recording in progress") } } func prepareAudioRecording() { var fileEnding = audioFormat.rawValue var fileType = AVFileType.m4a let encorder = fileEnding == AudioFormat.mp3.rawValue ? "aac" : fileEnding switch fileEnding { // todo: I'd like to store format info differently case AudioFormat.mp3.rawValue: fallthrough case AudioFormat.aac.rawValue: fallthrough case AudioFormat.alac.rawValue: fileEnding = "m4a" case AudioFormat.flac.rawValue: fileEnding = "flac"; fileType = .caf case AudioFormat.opus.rawValue: fileEnding = "ogg"; fileType = .caf default: assertionFailure("loaded unknown audio format: ".local + fileEnding) } let path = SCContext.getFilePath() if recordMic && SCContext.streamType == .systemaudio { SCContext.filePath = "\(path).qma" SCContext.filePath1 = "\(path).qma/sys.\(fileEnding)" SCContext.filePath2 = "\(path).qma/mic.\(fileEnding)" let infoJsonURL = "\(path).qma/info.json".url let jsonString = "{\"format\": \"\(fileEnding)\", \"encoder\": \"\(encorder)\", \"exportMP3\": \(audioFormat.rawValue == AudioFormat.mp3.rawValue), \"sysVol\": 1.0, \"micVol\": 1.0}" try? fd.createDirectory(at: SCContext.filePath.url, withIntermediateDirectories: true, attributes: nil) try? jsonString.write(to: infoJsonURL, atomically: true, encoding: .utf8) SCContext.audioFile = try! AVAudioFile(forWriting: SCContext.filePath1.url, settings: SCContext.updateAudioSettings(), commonFormat: .pcmFormatFloat32, interleaved: false) let sampleRate = SCContext.getSampleRate() ?? 48000 let settings = SCContext.updateAudioSettings(rate: sampleRate) SCContext.vW = try? AVAssetWriter.init(outputURL: SCContext.filePath2.url, fileType: fileType) SCContext.micInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: settings) SCContext.micInput.expectsMediaDataInRealTime = true if SCContext.vW.canAdd(SCContext.micInput) { SCContext.vW.add(SCContext.micInput) } SCContext.vW.startWriting() //SCContext.audioFile2 = try! AVAudioFile(forWriting: SCContext.filePath2.url, settings: settings, commonFormat: .pcmFormatFloat32, interleaved: false) } else { SCContext.filePath = "\(path).\(fileEnding)" SCContext.filePath1 = SCContext.filePath SCContext.audioFile = try! AVAudioFile(forWriting: SCContext.filePath.url, settings: SCContext.updateAudioSettings(), commonFormat: .pcmFormatFloat32, interleaved: false) } } } extension NSScreen { var displayID: CGDirectDisplayID? { return deviceDescription[NSDeviceDescriptionKey(rawValue: "NSScreenNumber")] as? CGDirectDisplayID } var isMainScreen: Bool { guard let id = self.displayID else { return false } return (CGDisplayIsMain(id) == 1) } } extension SCDisplay { var nsScreen: NSScreen? { return NSScreen.screens.first(where: { $0.displayID == self.displayID }) } } extension AppDelegate { func initVideo(conf: SCStreamConfiguration) { SCContext.startTime = nil let fileEnding = videoFormat.rawValue var fileType: AVFileType? switch fileEnding { case VideoFormat.mov.rawValue: fileType = AVFileType.mov case VideoFormat.mp4.rawValue: fileType = AVFileType.mp4 default: assertionFailure("loaded unknown video format".local) } if remuxAudio && recordMic && recordWinSound { SCContext.filePath = "\(SCContext.getFilePath()).\(fileEnding).\(fileEnding).\(fileEnding)" } else { SCContext.filePath = "\(SCContext.getFilePath()).\(fileEnding)" } SCContext.vW = try? AVAssetWriter.init(outputURL: SCContext.filePath.url, fileType: fileType!) let encoderIsH265 = (encoder.rawValue == Encoder.h265.rawValue) || recordHDR let fpsMultiplier: Double = Double(frameRate)/8 let encoderMultiplier: Double = encoderIsH265 ? 0.5 : 0.9 let resolution = Double(max(600, conf.width)) * Double(max(600, conf.height)) var qualityMultiplier = 1 - (log10(sqrt(resolution) * fpsMultiplier) / 5) switch videoQuality { case 0.3: qualityMultiplier = max(0.1, qualityMultiplier) case 0.7: qualityMultiplier = max(0.4, min(0.6, qualityMultiplier * 3)) default: qualityMultiplier = 1.0 } let h264Level = AVVideoProfileLevelH264HighAutoLevel let h265Level = recordHDR ? kVTProfileLevel_HEVC_Main10_AutoLevel : kVTProfileLevel_HEVC_Main_AutoLevel let targetBitrate = resolution * fpsMultiplier * encoderMultiplier * qualityMultiplier * (recordHDR ? 2 : 1) print("framerate set in app: \(frameRate)") print("target bitrate: \(targetBitrate/1000000)") var videoSettings: [String: Any] = [ AVVideoCodecKey: encoderIsH265 ? ((withAlpha && !recordHDR) ? AVVideoCodecType.hevcWithAlpha : AVVideoCodecType.hevc) : AVVideoCodecType.h264, // yes, not ideal if we want more than these encoders in the future, but it's ok for now AVVideoWidthKey: conf.width, AVVideoHeightKey: conf.height, AVVideoCompressionPropertiesKey: [ AVVideoProfileLevelKey: encoderIsH265 ? h265Level : h264Level, AVVideoAverageBitRateKey: max(200000, Int(targetBitrate)), AVVideoExpectedSourceFrameRateKey: frameRate, ] as [String : Any] ] if !recordHDR { videoSettings[AVVideoColorPropertiesKey] = [ AVVideoTransferFunctionKey: AVVideoTransferFunction_ITU_R_709_2, AVVideoColorPrimariesKey: AVVideoColorPrimaries_ITU_R_709_2, AVVideoYCbCrMatrixKey: AVVideoYCbCrMatrix_ITU_R_709_2] as [String : Any] } SCContext.vwInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings) SCContext.vwInput.expectsMediaDataInRealTime = true if SCContext.vW.canAdd(SCContext.vwInput) { SCContext.vW.add(SCContext.vwInput) } if #available(macOS 13, *) { SCContext.awInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: SCContext.updateAudioSettings()) SCContext.awInput.expectsMediaDataInRealTime = true if SCContext.vW.canAdd(SCContext.awInput) { SCContext.vW.add(SCContext.awInput) } } if recordMic { let sampleRate = SCContext.getSampleRate() ?? 48000 let settings = SCContext.updateAudioSettings(rate: sampleRate) SCContext.micInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: settings) SCContext.micInput.expectsMediaDataInRealTime = true if SCContext.vW.canAdd(SCContext.micInput) { SCContext.vW.add(SCContext.micInput) } startMicRecording() } SCContext.vW.startWriting() } func startMicRecording() { if micDevice == "default" { if enableAEC { var level = AUVoiceIOOtherAudioDuckingLevel.mid switch AECLevel { case "min": level = .min case "max": level = .max default: level = .mid } try? SCContext.AECEngine.startAudioStream(enableAEC: enableAEC, duckingLevel: level, audioBufferHandler: { pcmBuffer in if SCContext.isPaused || SCContext.startTime == nil { return } if SCContext.micInput.isReadyForMoreMediaData { SCContext.micInput.append(pcmBuffer.asSampleBuffer!) } }) } else { let input = SCContext.audioEngine.inputNode let inputFormat = input.inputFormat(forBus: 0) input.installTap(onBus: 0, bufferSize: 1024, format: inputFormat) { buffer, time in if SCContext.isPaused || SCContext.startTime == nil { return } if SCContext.micInput.isReadyForMoreMediaData { SCContext.micInput.append(buffer.asSampleBuffer!) } } try! SCContext.audioEngine.start() } } else { AudioRecorder.shared.setupAudioCapture() AudioRecorder.shared.start() } } func outputVideoEffectDidStart(for stream: SCStream) { DispatchQueue.main.async { camWindow.close() } print("[Presenter Overlay ON]") isPresenterON = true DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(poSafeDelay)) { self.isCameraReady = true } } func outputVideoEffectDidStop(for stream: SCStream) { print("[Presenter Overlay OFF]") presenterType = "OFF" isPresenterON = false isCameraReady = false DispatchQueue.main.async { if SCContext.stream != nil { camWindow.orderFront(self) } } } func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { if SCContext.saveFrame, let imageBuffer = sampleBuffer.imageBuffer { SCContext.saveFrame = false var ciImage = CIImage(cvPixelBuffer: imageBuffer) let url = "\(SCContext.getFilePath(capture: true)).png".url if !recordHDR { sampleBuffer.nsImage?.saveToFile(url) } else { let context = CIContext() // Create the HEIF destination with the correct UTI // if let destination = url? { // Specify format and color space (assuming default settings here) // let format = CIFormat.rgb10 let colorSpace = CGColorSpace(name: CGColorSpace.itur_2100_PQ) ?? CGColorSpaceCreateDeviceRGB() // let colorSpace = ciImage.colorSpace ?? CGColorSpaceCreateDeviceRGB() // Image exposure needs to be increased by one stop to match the original ciImage = ciImage.applyingFilter("CIExposureAdjust", parameters: ["inputEV": 1.0]) // context.writeHEIF10Representation(of: ciImage, to: destination as! URL, colorSpace: colorSpace) do{ // try context.writeHEIF10Representation(of:ciImage, // to:url, // colorSpace:colorSpace, // options: [ // kCGImageDestinationLossyCompressionQuality as CIImageRepresentationOption: 1.0 if #available(macOS 14.0, *) { try context.writePNGRepresentation(of:ciImage, to:url, format: .RGB10, colorSpace:colorSpace ) } else { // Fallback on earlier versions print("RGB10 PNG not supported on this macOS version") try context.writePNGRepresentation(of:ciImage, to:url, format: .RGBA8, colorSpace:colorSpace) } // try context.writePNGRepresentation(of:outImage, to:outURL, format: .RGBA16,colorSpace:colorSpace,options:[:]) } catch let error { // Handle the error case print("Error: \(error)") } // CGImageDestinationFinalize(destination) } } if SCContext.isPaused { return } guard sampleBuffer.isValid else { return } var SampleBuffer = sampleBuffer if SCContext.isResume { SCContext.isResume = false var pts = CMSampleBufferGetPresentationTimeStamp(SampleBuffer) guard let last = SCContext.lastPTS else { return } if last.flags.contains(CMTimeFlags.valid) { if SCContext.timeOffset.flags.contains(CMTimeFlags.valid) { pts = CMTimeSubtract(pts, SCContext.timeOffset) } let off = CMTimeSubtract(pts, last) print("adding \(CMTimeGetSeconds(off)) to \(CMTimeGetSeconds(SCContext.timeOffset)) (pts \(CMTimeGetSeconds(SCContext.timeOffset)))") if SCContext.timeOffset.value == 0 { SCContext.timeOffset = off } else { SCContext.timeOffset = CMTimeAdd(SCContext.timeOffset, off) } } SCContext.lastPTS?.flags = [] } switch outputType { case .screen: if (SCContext.screen == nil && SCContext.window == nil && SCContext.application == nil) || SCContext.streamType == .systemaudio { break } guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(SampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], let attachments = attachmentsArray.first else { return } guard let statusRawValue = attachments[SCStreamFrameInfo.status] as? Int, let status = SCFrameStatus(rawValue: statusRawValue), status == .complete else { return } if SCContext.vW != nil && SCContext.vW?.status == .writing, SCContext.startTime == nil { SCContext.startTime = Date.now SCContext.vW.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(SampleBuffer)) } if (SCContext.timeOffset.value > 0) { SampleBuffer = SCContext.adjustTime(sample: SampleBuffer, by: SCContext.timeOffset) ?? sampleBuffer } var pts = CMSampleBufferGetPresentationTimeStamp(SampleBuffer) let dur = CMSampleBufferGetDuration(SampleBuffer) if (dur.value > 0) { pts = CMTimeAdd(pts, dur) } if frameQueue.getArray().contains(where: { $0 >= pts }) { print("Skip this frame"); return } else { frameQueue.append(pts) } SCContext.lastPTS = pts if SCContext.vwInput.isReadyForMoreMediaData { if #available(macOS 14.2, *) { if let rect = attachments[.presenterOverlayContentRect] as? [String: Any]{ var type = "np" let off = (rect["X"] as! CGFloat == .infinity) let small = (rect["X"] as! CGFloat == 0.0) let big = (!off && !small) if off { type = "OFF" } else if small { type = "Small" } else if big { type = "Big" } if type != presenterType { print("Presenter Overlay set to \"\(type)\"!") isCameraReady = false DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(poSafeDelay)) { self.isCameraReady = true } presenterType = type } } } if isPresenterON && !isCameraReady { break } if SCContext.firstFrame == nil { SCContext.firstFrame = SampleBuffer } SCContext.vwInput.append(SampleBuffer) } break case .audio: if SCContext.streamType == .systemaudio { // write directly to file if not video recording hideMousePointer = true if SCContext.vW != nil && SCContext.vW?.status == .writing, SCContext.startTime == nil { SCContext.vW.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(SampleBuffer)) } if SCContext.startTime == nil { SCContext.startTime = Date.now } guard let samples = SampleBuffer.asPCMBuffer else { return } do { try SCContext.audioFile?.write(from: samples) } catch { assertionFailure("audio file writing issue".local) } } else { if SCContext.lastPTS == nil { return } if SCContext.awInput.isReadyForMoreMediaData { SCContext.awInput.append(SampleBuffer) } } #if compiler(>=6.0) case .microphone: break #endif @unknown default: assertionFailure("unknown stream type".local) } } func stream(_ stream: SCStream, didStopWithError error: Error) { // stream error print("closing stream with error:\n".local, error, "\nthis might be due to the window closing or the user stopping from the sonoma ui".local) DispatchQueue.main.async { SCContext.stream = nil SCContext.stopRecording() } } } class AudioRecorder: NSObject, AVCaptureAudioDataOutputSampleBufferDelegate { static let shared = AudioRecorder() private var captureSession: AVCaptureSession! private var audioInput: AVCaptureDeviceInput! private var audioDataOutput: AVCaptureAudioDataOutput! func setupAudioCapture() { captureSession = AVCaptureSession() // Get the default audio device (microphone) guard let audioDevice = SCContext.getCurrentMic() else { print("Unable to access microphone") return } // Create audio input do { audioInput = try AVCaptureDeviceInput(device: audioDevice) } catch { print("Unable to create audio input: \(error)") return } // Add audio input to capture session if captureSession.canAddInput(audioInput) { captureSession.addInput(audioInput) } else { print("Unable to add audio input to capture session") return } // Create audio data output audioDataOutput = AVCaptureAudioDataOutput() let audioQueue = DispatchQueue(label: "audioQueue") audioDataOutput.setSampleBufferDelegate(self, queue: audioQueue) // Add audio data output to capture session if captureSession.canAddOutput(audioDataOutput) { captureSession.addOutput(audioDataOutput) } else { print("Unable to add audio data output to capture session") return } } func start() { if let session = captureSession { session.startRunning() } } func stop() { if let session = captureSession { if session.isRunning { session.stopRunning() } } } func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { if SCContext.isPaused || SCContext.startTime == nil { return } if SCContext.micInput.isReadyForMoreMediaData { SCContext.micInput.append(sampleBuffer) } } } // https://developer.apple.com/documentation/screencapturekit/capturing_screen_content_in_macos // For Sonoma updated to https://developer.apple.com/forums/thread/727709 extension CMSampleBuffer { var asPCMBuffer: AVAudioPCMBuffer? { try? self.withAudioBufferList { audioBufferList, _ -> AVAudioPCMBuffer? in guard let absd = self.formatDescription?.audioStreamBasicDescription else { return nil } guard let format = AVAudioFormat(standardFormatWithSampleRate: absd.mSampleRate, channels: absd.mChannelsPerFrame) else { return nil } return AVAudioPCMBuffer(pcmFormat: format, bufferListNoCopy: audioBufferList.unsafePointer) } } var nsImage: NSImage? { return autoreleasepool { guard let pixelBuffer = CMSampleBufferGetImageBuffer(self) else { return nil } CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly) defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) } let ciImage = CIImage(cvPixelBuffer: pixelBuffer) let ciContext = CIContext() if let cgImage = ciContext.createCGImage(ciImage, from: ciImage.extent) { return NSImage(cgImage: cgImage, size: .zero) } return nil } } } // Based on https://gist.github.com/aibo-cora/c57d1a4125e145e586ecb61ebecff47c extension AVAudioPCMBuffer { var asSampleBuffer: CMSampleBuffer? { let asbd = self.format.streamDescription var sampleBuffer: CMSampleBuffer? = nil var format: CMFormatDescription? = nil guard CMAudioFormatDescriptionCreate( allocator: kCFAllocatorDefault, asbd: asbd, layoutSize: 0, layout: nil, magicCookieSize: 0, magicCookie: nil, extensions: nil, formatDescriptionOut: &format ) == noErr else { return nil } var timing = CMSampleTimingInfo( duration: CMTime(value: 1, timescale: Int32(asbd.pointee.mSampleRate)), presentationTimeStamp: CMClockGetTime(CMClockGetHostTimeClock()), decodeTimeStamp: .invalid ) guard CMSampleBufferCreate( allocator: kCFAllocatorDefault, dataBuffer: nil, dataReady: false, makeDataReadyCallback: nil, refcon: nil, formatDescription: format, sampleCount: CMItemCount(self.frameLength), sampleTimingEntryCount: 1, sampleTimingArray: &timing, sampleSizeEntryCount: 0, sampleSizeArray: nil, sampleBufferOut: &sampleBuffer ) == noErr else { return nil } guard CMSampleBufferSetDataBufferFromAudioBufferList( sampleBuffer!, blockBufferAllocator: kCFAllocatorDefault, blockBufferMemoryAllocator: kCFAllocatorDefault, flags: 0, bufferList: self.mutableAudioBufferList ) == noErr else { return nil } return sampleBuffer } } ================================================ FILE: QuickRecorder/SCContext.swift ================================================ // // SCContext.swift // QuickRecorder // // Created by apple on 2024/4/16. // import AVFAudio import AVFoundation import Foundation import ScreenCaptureKit import UserNotifications import SwiftLAME import SwiftUI import AECAudioStream class SCContext { static var trimingList = [URL]() static var firstFrame: CMSampleBuffer? static var autoStop = 0 static var recordCam = "" static var recordDevice = "" static var captureSession: AVCaptureSession! static var previewSession: AVCaptureSession! static var frameCache: CMSampleBuffer? static var filter: SCContentFilter? static var isMagnifierEnabled = false static var saveFrame = false static var isPaused = false static var isResume = false static var isSkipFrame = false static var lastPTS: CMTime? static var timeOffset = CMTimeMake(value: 0, timescale: 0) static var screenArea: NSRect? static let audioEngine = AVAudioEngine() static let AECEngine = AECAudioStream(sampleRate: 48000) static var backgroundColor: CGColor = CGColor.black static var filePath: String! static var filePath1: String! static var filePath2: String! static var audioFile: AVAudioFile? static var audioFile2: AVAudioFile? static var vW: AVAssetWriter! static var vwInput, awInput, micInput: AVAssetWriterInput! static var startTime: Date? static var timePassed: TimeInterval = 0 static var stream: SCStream! static var screen: SCDisplay? static var window: [SCWindow]? static var application: [SCRunningApplication]? static var streamType: StreamType? static var availableContent: SCShareableContent? static let excludedApps = ["", "com.apple.dock", "com.apple.screencaptureui", "com.apple.controlcenter", "com.apple.notificationcenterui", "com.apple.systemuiserver", "com.apple.WindowManager", "dev.mnpn.Azayaka", "com.gaosun.eul", "com.pointum.hazeover", "net.matthewpalmer.Vanilla", "com.dwarvesv.minimalbar", "com.bjango.istatmenus.status"] static func updateAvailableContentSync() -> SCShareableContent? { let semaphore = DispatchSemaphore(value: 0) var result: SCShareableContent? = nil updateAvailableContent { content in result = content semaphore.signal() } semaphore.wait() return result } private static func updateAvailableContent(completion: @escaping (SCShareableContent?) -> Void) { SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: true) { [self] content, error in if let error = error { switch error { case SCStreamError.userDeclined: DispatchQueue.global().asyncAfter(deadline: .now() + 1) { self.updateAvailableContent() {_ in} } default: print("Error: failed to fetch available content: ".local, error.localizedDescription) } completion(nil) // 在错误情况下返回 nil return } availableContent = content if let displays = content?.displays, !displays.isEmpty { completion(content) // 返回成功获取的 content } else { print("There needs to be at least one display connected!".local) completion(nil) // 如果没有显示器连接,则返回 nil } } } static func updateAvailableContent(completion: @escaping () -> Void) { SCShareableContent.getExcludingDesktopWindows(false, onScreenWindowsOnly: false) { content, error in if let error = error { switch error { case SCStreamError.userDeclined: requestPermissions() default: print("Error: failed to fetch available content: ".local, error.localizedDescription) } return } availableContent = content assert(availableContent?.displays.isEmpty != nil, "There needs to be at least one display connected!".local) completion() } } static func getSelf() -> SCRunningApplication? { return SCContext.availableContent!.applications.first(where: { Bundle.main.bundleIdentifier == $0.bundleIdentifier }) } static func getSelfWindows() -> [SCWindow]? { return SCContext.availableContent!.windows.filter( { guard let title = $0.title else { return false } return $0.owningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier && title != "Mouse Pointer".local && title != "Screen Magnifier".local && title != "Camera Overlayer".local && title != "iDevice Overlayer".local }) } static func getApps(isOnScreen: Bool = true, hideSelf: Bool = true) -> [SCRunningApplication] { var apps = [SCRunningApplication]() for app in getWindows(isOnScreen: isOnScreen, hideSelf: hideSelf).map({ $0.owningApplication }) { if !apps.contains(app!) { apps.append(app!) } } if hideSelf && ud.bool(forKey: "hideSelf") { apps = apps.filter({$0.bundleIdentifier != Bundle.main.bundleIdentifier}) } return apps } static func getWindows(isOnScreen: Bool = true, hideSelf: Bool = true) -> [SCWindow] { var windows = [SCWindow]() windows = availableContent!.windows.filter { guard let app = $0.owningApplication, let title = $0.title else {//, !title.isEmpty else { return false } return !excludedApps.contains(app.bundleIdentifier) && !title.contains("Item-0") && title != "Window" && $0.frame.width > 40 && $0.frame.height > 40 } if isOnScreen { windows = windows.filter({$0.isOnScreen == true}) } if hideSelf && ud.bool(forKey: "hideSelf") { windows = windows.filter({$0.owningApplication?.bundleIdentifier != Bundle.main.bundleIdentifier}) } return windows } static func getAppIcon(_ app: SCRunningApplication) -> NSImage? { if let appURL = NSWorkspace.shared.urlForApplication(withBundleIdentifier: app.bundleIdentifier) { let icon = NSWorkspace.shared.icon(forFile: appURL.path) icon.size = NSSize(width: 69, height: 69) return icon } let icon = NSImage(systemSymbolName: "questionmark.app.dashed", accessibilityDescription: "blank icon") icon!.size = NSSize(width: 69, height: 69) return icon } static func getScreenWithMouse() -> NSScreen? { let mouseLocation = NSEvent.mouseLocation let screenWithMouse = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }) return screenWithMouse } static func getSCDisplayWithMouse() -> SCDisplay? { if let displays = availableContent?.displays { for display in displays { if let currentDisplayID = getScreenWithMouse()?.displayID { if display.displayID == currentDisplayID { return display } } } } return nil } static func getFilePath(capture: Bool = false) -> String { let dateFormatter = DateFormatter() dateFormatter.dateFormat = "y-MM-dd HH.mm.ss" return ud.string(forKey: "saveDirectory")! + (capture ? "/Capturing at ".local : "/Recording at ".local) + dateFormatter.string(from: Date()) } static func updateAudioSettings(format: String = ud.string(forKey: "audioFormat") ?? "", rate: Int = 48000) -> [String : Any] { var audioSettings: [String : Any] = [AVSampleRateKey : rate, AVNumberOfChannelsKey : 2] // reset audioSettings var bitRate = ud.integer(forKey: "audioQuality") * 1000 if rate < 44100 { bitRate = min(64000, bitRate / 2) } switch format { case AudioFormat.mp3.rawValue: fallthrough case AudioFormat.aac.rawValue: audioSettings[AVFormatIDKey] = kAudioFormatMPEG4AAC audioSettings[AVEncoderBitRateKey] = bitRate case AudioFormat.alac.rawValue: audioSettings[AVFormatIDKey] = kAudioFormatAppleLossless audioSettings[AVEncoderBitDepthHintKey] = 16 case AudioFormat.flac.rawValue: audioSettings[AVFormatIDKey] = kAudioFormatFLAC case AudioFormat.opus.rawValue: audioSettings[AVFormatIDKey] = ud.string(forKey: "videoFormat") != VideoFormat.mp4.rawValue ? kAudioFormatOpus : kAudioFormatMPEG4AAC audioSettings[AVEncoderBitRateKey] = bitRate default: assertionFailure("unknown audio format while setting audio settings: ".local + (ud.string(forKey: "audioFormat") ?? "[no defaults]".local)) } return audioSettings } static func getBackgroundColor() -> CGColor { guard let color = ud.string(forKey: "background") else { return CGColor.black } if color == BackgroundType.wallpaper.rawValue { return CGColor.black } switch color { case "clear": backgroundColor = CGColor.clear case "black": backgroundColor = CGColor.black case "white": backgroundColor = CGColor.white case "gray": backgroundColor = NSColor.systemGray.cgColor case "yellow": backgroundColor = NSColor.systemYellow.cgColor case "orange": backgroundColor = NSColor.systemOrange.cgColor case "green": backgroundColor = NSColor.systemGreen.cgColor case "blue": backgroundColor = NSColor.systemBlue.cgColor case "red": backgroundColor = NSColor.systemRed.cgColor default: backgroundColor = ud.cgColor(forKey: "userColor") ?? CGColor.black } return backgroundColor } static func performMicCheck() async { guard ud.bool(forKey: "recordMic") == true else { return } if await AVCaptureDevice.requestAccess(for: .audio) { return } ud.setValue(false, forKey: "recordMic") DispatchQueue.main.async { let alert = createAlert(title: "Permission Required", message: "QuickRecorder needs permission to record your microphone.", button1: "Open Settings", button2: "Cancel") if alert.runModal() == .alertFirstButtonReturn { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone")!) } } } private static func requestPermissions() { DispatchQueue.main.async { let alert = createAlert(title: "Permission Required", message: "QuickRecorder needs screen recording permissions, even if you only intend on recording audio.", button1: "Open Settings", button2: "Cancel") if alert.runModal() == .alertFirstButtonReturn { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture")!) } NSApp.terminate(self) } } static func requestCameraPermission() { let status = AVCaptureDevice.authorizationStatus(for: .video) switch status { case .authorized, .restricted, .notDetermined: break case .denied: DispatchQueue.main.async { let alert = createAlert(title: "Permission Required", message: "QuickRecorder needs this permission to record your camera or mobile device.", button1: "Open Settings", button2: "Cancel") if alert.runModal() == .alertFirstButtonReturn { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera")!) } } @unknown default: break } } static func getWallpaper(_ display: SCDisplay) -> NSImage? { guard let screen = display.nsScreen else { return nil } guard let url = NSWorkspace.shared.desktopImageURL(for: screen) else { return nil } do { var wallpaper: NSImage? try wallpaper = NSImage(data: Data(contentsOf: url)) if let w = wallpaper { return w } } catch { print("load wallpaper error: \(error)") } return nil } static func getRecordingSize() -> String { do { let fileAttr = try fd.attributesOfItem(atPath: filePath) let byteFormat = ByteCountFormatter() byteFormat.allowedUnits = [.useMB] byteFormat.countStyle = .file return byteFormat.string(fromByteCount: fileAttr[FileAttributeKey.size] as! Int64) } catch { print(String(format: "failed to fetch file for size indicator: %@".local, error.localizedDescription)) } return "Unknown".local } static func getRecordingLength() -> String { let formatter = DateComponentsFormatter() formatter.allowedUnits = [.minute, .second] formatter.zeroFormattingBehavior = .pad formatter.unitsStyle = .positional if isPaused { return formatter.string(from: timePassed) ?? "Unknown".local } timePassed = Date.now.timeIntervalSince(startTime ?? Date.now) return formatter.string(from: timePassed) ?? "Unknown".local } static func isCameraRunning() -> Bool { var preview = false var capture = false if let session = previewSession { preview = session.isRunning } if let session = captureSession { capture = session.isRunning } return (preview || capture) } static func pauseRecording() { isPaused.toggle() PopoverState.shared.isPaused = isPaused if !isPaused { isResume = true startTime = Date.now.addingTimeInterval(-1) - SCContext.timePassed } } static func stopRecording() { if ud.bool(forKey: "preventSleep") { SleepPreventer.shared.allowSleep() } autoStop = 0 lastPTS = nil recordCam = "" recordDevice = "" isMagnifierEnabled = false mousePointer.orderOut(nil) screenMagnifier.orderOut(nil) AppDelegate.shared.stopGlobalMouseMonitor() if let w = NSApp.windows.first(where: { $0.title == "Area Overlayer".local }) { w.close() } if stream != nil { stream.stopCapture() } stream = nil if ud.bool(forKey: "recordMic") { micInput.markAsFinished() AudioRecorder.shared.stop() audioEngine.inputNode.removeTap(onBus: 0) audioEngine.stop() //DispatchQueue.global().async { try? audioEngine.inputNode.setVoiceProcessingEnabled(false) } if ud.bool(forKey: "enableAEC") { try? AECEngine.stopAudioUnit() } } if streamType != .systemaudio { let dispatchGroup = DispatchGroup() dispatchGroup.enter() vwInput.markAsFinished() if #available(macOS 13, *) { awInput.markAsFinished() } vW.finishWriting { if vW.status != .completed { print("Video writing failed with status: \(vW.status), error: \(String(describing: vW.error))") let err = vW.error?.localizedDescription ?? "Unknow Error" showNotification(title: "Failed to save file".local, body: "\(err)", id: "quickrecorder.error.\(UUID().uuidString)") } else { if ud.bool(forKey: "recordMic") && ud.bool(forKey: "recordWinSound") && ud.bool(forKey: "remuxAudio") { mixAudioTracks(videoURL: filePath.url) { result in switch result { case .success(let url): print("Exported video to \(String(describing: url.path))") if !ud.bool(forKey: "showPreview") { showNotification(title: "Recording Completed".local, body: String(format: "File saved to: %@".local, url.path), id: "quickrecorder.completed.\(UUID().uuidString)") } DispatchQueue.main.async { if ud.bool(forKey: "trimAfterRecord") { AppDelegate.shared.createNewWindow(view: VideoTrimmerView(videoURL: url), title: url.lastPathComponent, only: false) } else { showPreview(path: url.path) } } case .failure(let error): print("Failed to export video: \(error.localizedDescription)") } } } } dispatchGroup.leave() } dispatchGroup.wait() } else { if ud.bool(forKey: "recordMic") { vW.finishWriting {} } } DispatchQueue.main.async { controlPanel.close() if isCameraRunning() { if camWindow.isVisible { camWindow.close() } if deviceWindow.isVisible { deviceWindow.close() } if let preview = previewSession { preview.stopRunning() } if let capture = captureSession { capture.stopRunning() } } } audioFile = nil // close audio file audioFile2 = nil // close audio file2 if streamType == .systemaudio { if ud.string(forKey: "audioFormat") == AudioFormat.mp3.rawValue && !ud.bool(forKey: "recordMic") { Task { let outPutUrl = (String(filePath.dropLast(4)) + ".mp3").url do { try await m4a2mp3(inputUrl: filePath1.url, outputUrl: outPutUrl) try? fd.removeItem(atPath: filePath1) if !ud.bool(forKey: "showPreview") { let title = "Recording Completed".local let body = String(format: "File saved to: %@".local, outPutUrl.path.removingPercentEncoding!) let id = "quickrecorder.completed.\(UUID().uuidString)" showNotification(title: title, body: body, id: id) } else { DispatchQueue.main.async { showPreview(path: outPutUrl.path, image: NSImage(named: "audioIcon")) } } } catch { showNotification(title: "Failed to save file".local, body: "\(error.localizedDescription)", id: "quickrecorder.error.\(UUID().uuidString)") } } } else { if ud.bool(forKey: "remuxAudio") && ud.bool(forKey: "recordMic") { let fileURL = filePath.url let document = try? qmaPackageHandle.load(from: fileURL) if let document = document { let audioPlayerManager = AudioPlayerManager() audioPlayerManager.loadAudioFiles(format: document.info.format, package: fileURL, encoder: document.info.encoder, saveMP3: document.info.exportMP3) audioPlayerManager.sysVol = document.info.sysVol audioPlayerManager.micVol = document.info.micVol let exportMP3 = document.info.exportMP3 let format = exportMP3 ? "mp3" : document.info.format let saveURL = fileURL.deletingPathExtension().appendingPathExtension(format) audioPlayerManager.saveFile(saveURL, saveAsMP3: exportMP3) } } else { if !ud.bool(forKey: "showPreview") { let title = "Recording Completed".local let body = String(format: "File saved to: %@".local, filePath) let id = "quickrecorder.completed.\(UUID().uuidString)" showNotification(title: title, body: body, id: id) } else { showPreview(path: filePath, image: NSImage(named: "qmaIcon")) } } } } isPaused = false hideMousePointer = false window = nil screen = nil startTime = nil AppDelegate.shared.presenterType = "OFF" updateStatusBar() if !(ud.bool(forKey: "recordMic") && ud.bool(forKey: "recordWinSound") && ud.bool(forKey: "remuxAudio")) && streamType != .systemaudio { if let vW = vW { if vW.status != .completed { streamType = nil return } } if !ud.bool(forKey: "showPreview") { let title = "Recording Completed".local let body = String(format: "File saved to: %@".local, filePath) let id = "quickrecorder.completed.\(UUID().uuidString)" showNotification(title: title, body: body, id: id) } else { showPreview(path: filePath) } trimVideo() } streamType = nil firstFrame = nil } static func showPreview(path: String, image: NSImage? = nil) { if !ud.bool(forKey: "showPreview") { return } var previewImage: NSImage? let previewURL = fd.temporaryDirectory.appendingPathComponent("qr-preview.jpg") if image == nil { firstFrame?.nsImage?.saveToFile(previewURL, type: .jpeg) } if let i = image { previewImage = i } else { previewImage = NSImage(contentsOf: previewURL) } if let previewImage = previewImage, let screen = getScreenWithMouse() { let contentView = NSHostingView(rootView: PreviewView(frame: previewImage, filePath: path)) previewWindow.contentView = contentView previewWindow.setFrameOrigin(NSPoint(x: screen.frame.maxX - 280, y: screen.frame.minY + 20)) previewWindow.orderFront(self) } } static func m4a2mp3(inputUrl: URL, outputUrl: URL) async throws { let progress = Progress() let lameEncoder = try SwiftLameEncoder( sourceUrl: inputUrl, configuration: .init( sampleRate: .custom(48000), bitrateMode: .constant(Int32(ud.integer(forKey: "audioQuality"))), quality: .nearBest ), destinationUrl: outputUrl, progress: progress // optional ) try await lameEncoder.encode(priority: .userInitiated) } static func trimVideo() { if ud.bool(forKey: "trimAfterRecord") { let fileURL = filePath.url AppDelegate.shared.createNewWindow(view: VideoTrimmerView(videoURL: fileURL), title: fileURL.lastPathComponent, only: false) } } static func getCameras() -> [AVCaptureDevice] { let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .externalUnknown], mediaType: .video, position: .unspecified) return discoverySession.devices } static func getMicrophone() -> [AVCaptureDevice] { var discoverySession: AVCaptureDevice.DiscoverySession if #available(macOS 15.0, *) { discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInMicrophone, .microphone], mediaType: .audio, position: .unspecified) } else { discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInMicrophone, .externalUnknown], mediaType: .audio, position: .unspecified) } return discoverySession.devices.filter({ !$0.localizedName.contains("CADefaultDeviceAggregate") }) } static func getiDevice() -> [AVCaptureDevice] { let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.externalUnknown], mediaType: .muxed, position: .unspecified) return discoverySession.devices } static func getCurrentMic() -> AVCaptureDevice? { let deviceName = ud.string(forKey: "micDevice") return getMicrophone().first(where: { $0.localizedName == deviceName }) } /*static func getChannelCount() -> Int? { if let device = getCurrentMic() { if let channels = device.formats.first?.formatDescription.audioChannelLayout?.numberOfChannels { return channels } let activeFormat = device.activeFormat let description = activeFormat.formatDescription if let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(description)?.pointee { let channelCount = audioStreamBasicDescription.mChannelsPerFrame return max(2, Int(channelCount)) } } return getDefaultChannelCount() } static func getDefaultChannelCount() -> Int? { var deviceID = AudioObjectID(0) var propertySize = UInt32(MemoryLayout.size(ofValue: deviceID)) // 获取默认音频输入设备 var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let status = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &deviceID ) guard status == noErr else { print("Failed to get default audio input device") return nil } // 获取通道数 address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyStreamConfiguration, mScope: kAudioDevicePropertyScopeInput, mElement: kAudioObjectPropertyElementMain ) // 查询流配置信息 var streamConfig: UnsafeMutableAudioBufferListPointer? propertySize = 0 // 先获取属性大小 let sizeStatus = AudioObjectGetPropertyDataSize(deviceID, &address, 0, nil, &propertySize) guard sizeStatus == noErr else { print("Failed to get size for stream configuration") return nil } // 分配内存以存储音频流配置 let bufferList = UnsafeMutablePointer.allocate(capacity: Int(propertySize)) defer { bufferList.deallocate() } let configStatus = AudioObjectGetPropertyData(deviceID, &address, 0, nil, &propertySize, bufferList) guard configStatus == noErr else { print("Failed to get stream configuration") return nil } streamConfig = UnsafeMutableAudioBufferListPointer(bufferList) // 计算通道总数 var totalChannels = 0 for buffer in streamConfig! { totalChannels += Int(buffer.mNumberChannels) } return max(2, totalChannels) }*/ static func getSampleRate() -> Int? { if let device = getCurrentMic() { let activeFormat = device.activeFormat let description = activeFormat.formatDescription if let audioStreamBasicDescription = CMAudioFormatDescriptionGetStreamBasicDescription(description)?.pointee { let sampleRate = audioStreamBasicDescription.mSampleRate return Int(sampleRate) } } return getDefaultSampleRate() } static func getDefaultSampleRate() -> Int? { var deviceID = AudioObjectID(0) var propertySize = UInt32(MemoryLayout.size(ofValue: deviceID)) // 获取默认音频输入设备 var address = AudioObjectPropertyAddress( mSelector: kAudioHardwarePropertyDefaultInputDevice, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let status = AudioObjectGetPropertyData( AudioObjectID(kAudioObjectSystemObject), &address, 0, nil, &propertySize, &deviceID ) guard status == noErr else { print("Failed to get default audio input device") return nil } // 获取采样率 var sampleRate: Double = 0 propertySize = UInt32(MemoryLayout.size(ofValue: sampleRate)) address = AudioObjectPropertyAddress( mSelector: kAudioDevicePropertyNominalSampleRate, mScope: kAudioObjectPropertyScopeGlobal, mElement: kAudioObjectPropertyElementMain ) let sampleRateStatus = AudioObjectGetPropertyData( deviceID, &address, 0, nil, &propertySize, &sampleRate ) guard sampleRateStatus == noErr else { print("Failed to get sample rate for the default input device") return nil } return Int(sampleRate) } static func adjustTime(sample: CMSampleBuffer, by offset: CMTime) -> CMSampleBuffer? { guard CMSampleBufferGetFormatDescription(sample) != nil else { return nil } var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: Int(CMSampleBufferGetNumSamples(sample))) CMSampleBufferGetSampleTimingInfoArray(sample, entryCount: timingInfo.count, arrayToFill: &timingInfo, entriesNeededOut: nil) for i in 0..) -> Void) { showNotification(title: "Still Processing".local, body: "Mixing audio track...".local, id: "quickrecorder.processing.\(UUID().uuidString)") let asset = AVAsset(url: videoURL) let audioOutputURL = videoURL.deletingPathExtension() let outputURL = audioOutputURL.deletingPathExtension() let audioOnlyComposition = AVMutableComposition() let fileEnding = ud.string(forKey: "videoFormat") ?? "" var fileType: AVFileType? switch fileEnding { case VideoFormat.mov.rawValue: fileType = AVFileType.mov case VideoFormat.mp4.rawValue: fileType = AVFileType.mp4 default: assertionFailure("loaded unknown video format".local) } let audioTracks = asset.tracks(withMediaType: .audio) guard audioTracks.count > 1 else { completion(.failure(NSError(domain: "AudioTrackError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Not enough audio tracks found."]))) return } for audioTrack in audioTracks { if let compositionAudioTrack = audioOnlyComposition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { do { try compositionAudioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: audioTrack, at: .zero) } catch { completion(.failure(NSError(domain: "AudioTrackInsertionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to insert audio track: \(error.localizedDescription)"]))) return } } } let audioMix = AVMutableAudioMix() audioMix.inputParameters = audioTracks.map { let parameters = AVMutableAudioMixInputParameters(track: $0) parameters.trackID = $0.trackID return parameters } guard let audioExportSession = AVAssetExportSession(asset: audioOnlyComposition, presetName: AVAssetExportPresetHighestQuality) else { completion(.failure(NSError(domain: "AudioExportSessionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create audio export session."]))) return } audioExportSession.outputURL = audioOutputURL audioExportSession.outputFileType = fileType ?? .mp4 audioExportSession.audioMix = audioMix audioExportSession.exportAsynchronously { /*var exportStatus: AVAssetExportSession.Status = .unknown // Loop until export session is completed, failed, or cancelled while exportStatus != .completed && exportStatus != .failed && exportStatus != .cancelled { exportStatus = audioExportSession.status Thread.sleep(forTimeInterval: 0.1) }*/ switch audioExportSession.status { case .completed: let audioAsset = AVAsset(url: audioOutputURL) let composition = AVMutableComposition() guard let videoTrack = asset.tracks(withMediaType: .video).first, let compositionVideoTrack = composition.addMutableTrack(withMediaType: .video, preferredTrackID: kCMPersistentTrackID_Invalid) else { completion(.failure(NSError(domain: "VideoTrackError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to get video track."]))) return } do { try compositionVideoTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: videoTrack, at: .zero) } catch { completion(.failure(NSError(domain: "VideoTrackInsertionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to insert video track: \(error.localizedDescription)"]))) return } let audioTracks = audioAsset.tracks(withMediaType: .audio) guard audioTracks.count >= 1 else { completion(.failure(NSError(domain: "AudioTrackError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Not enough audio tracks found."]))) return } for audioTrack in audioTracks { if let compositionAudioTrack = composition.addMutableTrack(withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) { do { try compositionAudioTrack.insertTimeRange(CMTimeRange(start: .zero, duration: asset.duration), of: audioTrack, at: .zero) } catch { completion(.failure(NSError(domain: "AudioTrackInsertionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to insert audio track: \(error.localizedDescription)"]))) return } } } guard let exportSession = AVAssetExportSession(asset: composition, presetName: AVAssetExportPresetPassthrough) else { completion(.failure(NSError(domain: "ExportSessionError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to create export session."]))) return } exportSession.outputURL = outputURL exportSession.outputFileType = fileType ?? .mp4 exportSession.audioMix = audioMix exportSession.exportAsynchronously { switch exportSession.status { case .completed: let fileManager = fd try? fileManager.removeItem(atPath: filePath) try? fileManager.removeItem(atPath: audioOutputURL.path) completion(.success(outputURL)) case .failed: completion(.failure(exportSession.error ?? NSError(domain: "ExportError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export failed for an unknown reason."]))) case .cancelled: completion(.failure(NSError(domain: "ExportCancelled", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export was cancelled."]))) default: break } } case .failed: completion(.failure(audioExportSession.error ?? NSError(domain: "ExportError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export failed for an unknown reason."]))) case .cancelled: completion(.failure(NSError(domain: "ExportCancelled", code: -1, userInfo: [NSLocalizedDescriptionKey: "Export was cancelled."]))) default: break } } } } ================================================ FILE: QuickRecorder/Supports/AppleScript.swift ================================================ // // AppleScript.swift // QuickRecorder // // Created by apple on 2024/9/23. // import Foundation import AppKit import ScreenCaptureKit class selectScreen: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } SCContext.updateAvailableContent { DispatchQueue.main.async { closeAllWindow() if var index = self.evaluatedArguments!["index"] as? Int { guard let screens = SCContext.availableContent?.displays else { return } index -= 1 closeAllWindow() if index >= screens.count || index < 0 { createAlert(title: "Error".local, message: "Invalid screen number!".local, button1: "OK".local).runModal() return } else { let screen = screens[index] AppDelegate.shared.createCountdownPanel(screen: screen) { AppDelegate.shared.prepRecord(type: "display", screens: screen, windows: nil, applications: nil) } } } else { closeAllWindow() AppDelegate.shared.createNewWindow(view: ScreenSelector(), title: "Screen Selector".local) } } } return nil } } class selectArea: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } SCContext.updateAvailableContent { DispatchQueue.main.async { closeAllWindow() DispatchQueue.main.async { AppDelegate.shared.showAreaSelector(size: NSSize(width: 600, height: 450)) var currentDisplay = SCContext.getSCDisplayWithMouse() mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .rightMouseDown, .leftMouseDown, .otherMouseDown]) { event in let display = SCContext.getSCDisplayWithMouse() if display != currentDisplay { currentDisplay = display closeAllWindow() AppDelegate.shared.showAreaSelector(size: NSSize(width: 600, height: 450)) } } } } } return nil } } class selectApps: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } SCContext.updateAvailableContent { DispatchQueue.main.async { closeAllWindow() if let name = self.evaluatedArguments!["name"] as? String { guard let app = SCContext.availableContent?.applications.first(where: { $0.applicationName == name }) else { createAlert(title: "Error".local, message: "No such application!".local, button1: "OK".local).runModal() return } closeAllWindow() guard let screens = SCContext.availableContent?.displays else { return } guard let windows = SCContext.availableContent?.windows.filter({ guard let title = $0.title else { return false } return !title.contains("Item-0") && title != "Window" && $0.frame.width > 40 && $0.frame.height > 40 }) else { return } var s = [SCDisplay]() for screen in screens { for w in windows { if NSIntersectsRect(screen.frame, w.frame) { if !s.contains(screen) { s.append(screen) }} } } if s.isEmpty { createAlert(title: "Error".local, message: "This application has no windows!".local, button1: "OK".local).runModal() return } if s.count != 1 { AppDelegate.shared.createNewWindow(view: AppSelector(), title: "App Selector".local) createAlert(title: "Error".local, message: "This app exists in multiple screens, please select it manually!".local, button1: "OK".local).runModal() } else { AppDelegate.shared.createCountdownPanel(screen: s.first!) { AppDelegate.shared.prepRecord(type: "application", screens: s.first!, windows: nil, applications: [app]) } } } else { closeAllWindow() AppDelegate.shared.createNewWindow(view: AppSelector(), title: "App Selector".local) } } } return nil } } class selectWindows: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } SCContext.updateAvailableContent { DispatchQueue.main.async { closeAllWindow() if let title = self.evaluatedArguments!["title"] as? String { var windows = [SCWindow]() guard let w = SCContext.availableContent?.windows.filter({ $0.title == title }) else { return } windows = w if let app = self.evaluatedArguments!["app"] as? String { guard let w = SCContext.availableContent?.windows.filter({ $0.title == title && $0.owningApplication?.applicationName == app }) else { return } windows = w } closeAllWindow() if windows.isEmpty { createAlert(title: "Error".local, message: "No such window!".local, button1: "OK".local).runModal() return } if windows.count > 1 { AppDelegate.shared.createNewWindow(view: WinSelector(), title: "Window Selector".local) createAlert(title: "Error".local, message: "Duplicate window exists, please select it manually!".local, button1: "OK".local).runModal() return } let window = windows.first! guard let screens = SCContext.availableContent?.displays else { return } var s = [SCDisplay]() for screen in screens { if NSIntersectsRect(screen.frame, window.frame) { if !s.contains(screen) { s.append(screen) }} } if s.isEmpty { createAlert(title: "Error".local, message: "Unable to find the screen this window belongs to!".local, button1: "OK".local).runModal() return } if let display = SCContext.getSCDisplayWithMouse() { if s.contains(display) { AppDelegate.shared.createCountdownPanel(screen: display) { AppDelegate.shared.prepRecord(type: "window" , screens: s.first!, windows: [window], applications: nil) } } else { AppDelegate.shared.createCountdownPanel(screen: s.first!) { AppDelegate.shared.prepRecord(type: "window" , screens: s.first!, windows: [window], applications: nil) } } } } else { closeAllWindow() AppDelegate.shared.createNewWindow(view: WinSelector(), title: "Window Selector".local) } } } return nil } } class recordAudio: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } SCContext.updateAvailableContent { DispatchQueue.main.async { let m = UserDefaults.standard.bool(forKey: "recordMic") if let mic = self.evaluatedArguments!["mic"] as? Bool { UserDefaults.standard.set(mic, forKey: "recordMic") } closeAllWindow() AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) UserDefaults.standard.set(m, forKey: "recordMic") } } return nil } } class setPreferences: NSScriptCommand { override func performDefaultImplementation() -> Any? { if SCContext.stream != nil { createAlert(title: "Error".local, message: "Already recording!".local, button1: "OK".local).runModal() return nil } if let hires = self.evaluatedArguments!["hires"] as? Bool { UserDefaults.standard.set(hires, forKey: "highRes") } if let fps = self.evaluatedArguments!["fps"] as? Int { UserDefaults.standard.set(fps, forKey: "frameRate") } if let cursor = self.evaluatedArguments!["cursor"] as? Bool { UserDefaults.standard.set(cursor, forKey: "showMouse") } if let sound = self.evaluatedArguments!["sound"] as? Bool { UserDefaults.standard.set(sound, forKey: "recordWinSound") } if let microphone = self.evaluatedArguments!["microphone"] as? Bool { UserDefaults.standard.set(microphone, forKey: "recordMic") } if let quality = self.evaluatedArguments!["quality"] as? Int { if [1,2,3].contains(quality) { switch quality { case 1: UserDefaults.standard.set(0.3, forKey: "videoQuality") case 2: UserDefaults.standard.set(0.7, forKey: "videoQuality") default: UserDefaults.standard.set(1.0, forKey: "videoQuality") } } } if let micname = self.evaluatedArguments!["micname"] as? String { if SCContext.getMicrophone().map({$0.localizedName}).contains(micname) || micname == "default" { UserDefaults.standard.set(micname, forKey: "micDevice") } } if let hdr = self.evaluatedArguments!["hdr"] as? Bool { if #available(macOS 15.0, *) { UserDefaults.standard.set(hdr, forKey: "recordHDR") } } return nil } } ================================================ FILE: QuickRecorder/Supports/GroupForm.swift ================================================ // // GroupForm.swift // AirBattery // // Created by apple on 2024/10/28. // import SwiftUI struct HoverButton: View { var color: Color = .primary var secondaryColor: Color = .blue var action: () -> Void @ViewBuilder let label: () -> Content @State private var isHovered: Bool = false var body: some View { Button(action: { action() }, label: { label().foregroundStyle(isHovered ? secondaryColor : color) }) .buttonStyle(.plain) .onHover(perform: { isHovered = $0 }) } } struct SForm: View { var spacing: CGFloat = 30 var noSpacer: Bool = false @ViewBuilder let content: () -> Content var body: some View { VStack(spacing: spacing) { content() if !noSpacer { Spacer().frame(minHeight: 0) } } .padding(.bottom, noSpacer ? 0 : -spacing) .padding() .frame(maxWidth: .infinity) } } struct SGroupBox: View { var label: LocalizedStringKey? = nil @ViewBuilder let content: () -> Content var body: some View { GroupBox(label: label != nil ? Text(label!).font(.headline) : nil) { VStack(spacing: 10) { content() }.padding(5) } } } struct SItem: View { var label: LocalizedStringKey? = nil var spacing: CGFloat = 8 @ViewBuilder let content: () -> Content var body: some View { HStack(spacing: spacing) { if let label = label { Text(label) } Spacer() content() }.frame(height: 16) } } struct SDivider: View { var body: some View { Divider().opacity(0.5) } } struct SSlider: View { var label: LocalizedStringKey? = nil @Binding var value: Int var range: ClosedRange = 0...100 var width: CGFloat = .infinity var body: some View { HStack { if let label = label { Text(label) } Spacer() Slider(value: Binding(get: { Double(value) }, set: { newValue in let base: Int = Int(newValue.rounded()) let modulo: Int = base % 1 value = base - modulo }), in: range).frame(maxWidth: width) }.frame(height: 16) } } struct SInfoButton: View { var tips: LocalizedStringKey @State private var isPresented: Bool = false var body: some View { Button(action: { isPresented = true }, label: { Image(systemName: "info.circle") .font(.system(size: 15, weight: .light)) .opacity(0.5) }) .buttonStyle(.plain) .onChange(of: isPresented) {_ in} .sheet(isPresented: $isPresented) { VStack(alignment: .trailing) { GroupBox { Text(tips).padding() } Button(action: { isPresented = false }, label: { Text("OK").frame(width: 30) }).keyboardShortcut(.defaultAction) }.padding() } } } struct SButton: View { var title: LocalizedStringKey var buttonTitle: LocalizedStringKey var tips: LocalizedStringKey? var action: () -> Void init(_ title: LocalizedStringKey, buttonTitle: LocalizedStringKey, tips: LocalizedStringKey? = nil, action: @escaping () -> Void) { self.title = title self.buttonTitle = buttonTitle self.tips = tips self.action = action } var body: some View { HStack(spacing: 4) { Text(title) Spacer() if let tips = tips { SInfoButton(tips: tips) } Button(buttonTitle, action: { action() }) }.frame(height: 16) } } struct SField: View { var title: LocalizedStringKey var placeholder: LocalizedStringKey var tips: LocalizedStringKey? @Binding var text: String var width: Double init(_ title: LocalizedStringKey, placeholder:LocalizedStringKey = "", tips: LocalizedStringKey? = nil, text: Binding, width: Double = .infinity) { self.title = title self.placeholder = placeholder self.tips = tips self._text = text self.width = width } var body: some View { HStack(spacing: 4) { Text(title) Spacer() if let tips = tips { SInfoButton(tips: tips) } TextField(placeholder, text: $text) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) .frame(maxWidth: width) } } } struct SPicker: View { var title: LocalizedStringKey @Binding var selection: T var style: Style var tips: LocalizedStringKey? @ViewBuilder let content: () -> Content init(_ title: LocalizedStringKey, selection: Binding, style: Style = .menu, tips: LocalizedStringKey? = nil, @ViewBuilder content: @escaping () -> Content) { self.title = title self._selection = selection self.style = style self.tips = tips self.content = content } var body: some View { HStack { Text(title) Spacer() if let tips = tips { SInfoButton(tips: tips) } Picker(selection: $selection, content: { content() }, label: {}) .fixedSize() .pickerStyle(style) .buttonStyle(.borderless) }.frame(height: 16) } } struct SToggle: View { var title: LocalizedStringKey @Binding var isOn: Bool var tips: LocalizedStringKey? init(_ title: LocalizedStringKey, isOn: Binding, tips: LocalizedStringKey? = nil) { self.title = title self._isOn = isOn self.tips = tips } var body: some View { HStack(spacing: 4) { Text(title) Spacer() if let tips = tips { SInfoButton(tips: tips) } Toggle("", isOn: $isOn) .toggleStyle(.switch) .scaleEffect(0.7) .frame(width: 32) }.frame(height: 16) } } struct SSteper: View { var title: LocalizedStringKey @Binding var value: Int var min: Int var max: Int var width: CGFloat var tips: LocalizedStringKey? init(_ title: LocalizedStringKey, value: Binding, min: Int = 0, max: Int = 100, width: CGFloat = 45, tips: LocalizedStringKey? = nil) { self.title = title self._value = value self.tips = tips self.width = width self.min = min self.max = max } var body: some View { HStack(spacing: 0) { Text(title) Spacer() if let tips = tips { SInfoButton(tips: tips) } TextField("", value: $value, formatter: NumberFormatter()) .textFieldStyle(.roundedBorder) .multilineTextAlignment(.trailing) .frame(width: width) .onChange(of: value) { newValue in if newValue > max { value = max } if newValue < min { value = min } } Stepper("", value: $value) .padding(.leading, -6) }.frame(height: 16) } } ================================================ FILE: QuickRecorder/Supports/Scriptable.sdef ================================================ ================================================ FILE: QuickRecorder/Supports/SleepPreventer.swift ================================================ // // SleepPreventer.swift // QuickRecorder // // Created by apple on 2024/12/9. // import Foundation import IOKit.pwr_mgt class SleepPreventer { static let shared = SleepPreventer() private var assertionID: IOPMAssertionID = 0 func preventSleep(reason: String) { let type = "PreventUserIdleDisplaySleep" as CFString let reason = reason as CFString let result = IOPMAssertionCreateWithName(type, IOPMAssertionLevel(kIOPMAssertionLevelOn), reason, &assertionID) if result != kIOReturnSuccess { print("Failure to prevent sleep, error: \(result)") } } func allowSleep() { let result = IOPMAssertionRelease(assertionID) if result != kIOReturnSuccess { print("Failed to release assertion, error: \(result)") } } } ================================================ FILE: QuickRecorder/Supports/Sparkle.swift ================================================ // // Sparkle.swift // QuickRecorder // // Created by apple on 2024/5/2. // import SwiftUI import Sparkle // This view model class publishes when new updates can be checked by the user final class CheckForUpdatesViewModel: ObservableObject { @Published var canCheckForUpdates = false init(updater: SPUUpdater) { updater.publisher(for: \.canCheckForUpdates) .assign(to: &$canCheckForUpdates) } } // This is the view for the Check for Updates menu item // Note this intermediate view is necessary for the disabled state on the menu item to work properly before Monterey. // See https://stackoverflow.com/questions/68553092/menu-not-updating-swiftui-bug for more info struct CheckForUpdatesView: View { @ObservedObject private var checkForUpdatesViewModel: CheckForUpdatesViewModel private let updater: SPUUpdater init(updater: SPUUpdater) { self.updater = updater // Create our view model for our CheckForUpdatesView self.checkForUpdatesViewModel = CheckForUpdatesViewModel(updater: updater) } var body: some View { Button("Check for Updates…", action: updater.checkForUpdates) .disabled(!checkForUpdatesViewModel.canCheckForUpdates) } } struct UpdaterSettingsView: View { private let updater: SPUUpdater @State private var automaticallyChecksForUpdates: Bool @State private var automaticallyDownloadsUpdates: Bool init(updater: SPUUpdater) { self.updater = updater self.automaticallyChecksForUpdates = updater.automaticallyChecksForUpdates self.automaticallyDownloadsUpdates = updater.automaticallyDownloadsUpdates } var body: some View { SToggle("Automatically check for updates", isOn: $automaticallyChecksForUpdates) .onChange(of: automaticallyChecksForUpdates) { newValue in updater.automaticallyChecksForUpdates = newValue } Divider().opacity(0.5) SToggle("Automatically download updates", isOn: $automaticallyDownloadsUpdates) .disabled(!automaticallyChecksForUpdates) .onChange(of: automaticallyDownloadsUpdates) { newValue in updater.automaticallyDownloadsUpdates = newValue } } } ================================================ FILE: QuickRecorder/Supports/WindowAccessor.swift ================================================ // // WindowAccessor.swift // xHistory // // Created by apple on 2024/11/7. // import AppKit import SwiftUI struct WindowAccessor: NSViewRepresentable { var onWindowOpen: ((NSWindow?) -> Void)? var onWindowActive: ((NSWindow?) -> Void)? var onWindowDeactivate: ((NSWindow?) -> Void)? var onWindowClose: (() -> Void)? func makeNSView(context: Context) -> NSView { let view = NSView() DispatchQueue.main.async { if let window = view.window { window.delegate = context.coordinator context.coordinator.window = window self.onWindowOpen?(window) } else { self.onWindowOpen?(nil) } } return view } func updateNSView(_ nsView: NSView, context: Context) {} func makeCoordinator() -> Coordinator { Coordinator( onWindowOpen: onWindowOpen, onWindowActive: onWindowActive, onWindowDeactivate: onWindowDeactivate, onWindowClose: onWindowClose ) } class Coordinator: NSObject, NSWindowDelegate { weak var window: NSWindow? // 使用 weak 避免循环引用 var onWindowOpen: ((NSWindow?) -> Void)? var onWindowActive: ((NSWindow?) -> Void)? var onWindowDeactivate: ((NSWindow?) -> Void)? var onWindowClose: (() -> Void)? init(onWindowOpen: ((NSWindow?) -> Void)? = nil, onWindowActive: ((NSWindow?) -> Void)? = nil, onWindowDeactivate: ((NSWindow?) -> Void)? = nil, onWindowClose: (() -> Void)? = nil) { self.onWindowOpen = onWindowOpen self.onWindowClose = onWindowClose self.onWindowActive = onWindowActive self.onWindowDeactivate = onWindowDeactivate } func windowWillClose(_ notification: Notification) { onWindowClose?() } func windowDidBecomeKey(_ notification: Notification) { onWindowActive?(window) } func windowDidResignKey(_ notification: Notification) { onWindowDeactivate?(window) } } } ================================================ FILE: QuickRecorder/Supports/WindowHighlighter.swift ================================================ // // WindowHighlighter.swift // Topit // // Created by apple on 2024/11/26. // import SwiftUI import ScreenCaptureKit struct CoverView: View { var body: some View { Color.clear.overlay { Rectangle().stroke(.blue, lineWidth: 5) } } } struct HighlightMask: View { let app: String let title: String let windowID: Int var appDelegate = AppDelegate.shared @State var window: SCWindow? @State var display: SCDisplay? @State var color: Color = .blue @State var showSheet: Bool = false @State private var isPopoverShowing = false @State private var disableFilter = false @State private var donotCapture = false @State private var autoStop = 0 var body: some View { color .opacity(0.2) .cornerRadius(10) .help("\(app) - \(title)") .sheet(isPresented: $showSheet) { HStack(spacing: 4) { Button(action: { showSheet = false }, label: { VStack{ Image(systemName: "xmark.circle.fill") .font(.system(size: 36)) .foregroundStyle(.gray) Text("Cancel") .foregroundStyle(.secondary) .font(.system(size: 12)) } }).buttonStyle(.plain) Spacer() OptionsView() Spacer() Button(action: { isPopoverShowing = true }, label: { Image(systemName: "timer") .font(.system(size: 11, weight: .bold)) .foregroundStyle(.blue) }) .buttonStyle(.plain) .padding(.top, 42.5) .popover(isPresented: $isPopoverShowing, arrowEdge: .bottom, content: { HStack { Text(" Stop after".local) TextField("", value: $autoStop, formatter: NumberFormatter()) .textFieldStyle(RoundedBorderTextFieldStyle()) Stepper("", value: $autoStop) .padding(.leading, -10) Text("minutes ".local) } .fixedSize() .padding() }) Button(action: { startRecording() }, label: { VStack{ Image(systemName: "record.circle.fill") .font(.system(size: 36)) .foregroundStyle(.red) Text("Start") .foregroundStyle(.secondary) .font(.system(size: 12)) } }).buttonStyle(.plain) } .focusable(false) .frame(width: 640, height: 90) .padding(.horizontal, 40) .onDisappear { if let mask = WindowHighlighter.shared.mask { mask.close() } } } .onPressGesture { if let w = WindowHighlighter.shared.getSCWindowWithID(UInt32(windowID)), let d = SCContext.getSCDisplayWithMouse() { display = d window = w WindowHighlighter.shared.stopMouseMonitor() showSheet = true return } color = .red withAnimation(.easeInOut(duration: 0.6)) { color = .blue } } } func startRecording() { closeAllWindow() switch WindowHighlighter.shared.Mode { case 2: var dashWindow = NSWindow() guard let screen = display, let nsScreen = display?.nsScreen, var area = window?.frame else { return } area = CGRectTransform(cgRect: area) SCContext.screenArea = NSRect(x: area.origin.x - nsScreen.frame.minX, y: area.origin.y - nsScreen.frame.minY, width:area.width, height: area.height) let frame = NSRect(x: Int(area.origin.x - 3), y: Int(area.origin.y - 3), width: Int(area.width + 6), height: Int(area.height + 6)) dashWindow = NSWindow(contentRect: frame, styleMask: [.fullSizeContentView], backing: .buffered, defer: false) dashWindow.hasShadow = false dashWindow.level = .screenSaver dashWindow.ignoresMouseEvents = true dashWindow.isReleasedWhenClosed = false dashWindow.title = "Area Overlayer".local dashWindow.backgroundColor = NSColor.clear dashWindow.contentView = NSHostingView(rootView: DashWindow()) dashWindow.orderFront(self) appDelegate.createCountdownPanel(screen: screen) { SCContext.autoStop = autoStop appDelegate.prepRecord(type: "area", screens: display, windows: nil, applications: nil) } default: if let d = display, let w = window { appDelegate.createCountdownPanel(screen: d) { SCContext.autoStop = autoStop appDelegate.prepRecord(type: "window" , screens: d, windows: [w], applications: nil) } } } } } class WindowHighlighter { static let shared = WindowHighlighter() var mouseMonitor: Any? var mouseMonitorL: Any? var targetWindowID: Int? var mask: EscPanel? var Mode: Int = 1 func registerMouseMonitor(mode: Int = 1) { closeAllWindow() Mode = mode DispatchQueue.main.async { var message = "" var id = "" switch mode { case 2: id = "qr.how-to-select.note2" message = "Click on a window to select its area\nor press Esc to cancel.".local default: message = "Click the window you want to record\nor press Esc to cancel.".local id = "qr.how-to-select.note" } tips(message, id: id) } for screen in NSScreen.screens { let cover = EscPanel(contentRect: screen.frame, styleMask: [.nonactivatingPanel, .fullSizeContentView], backing: .buffered, defer: false) cover.contentView = NSHostingView(rootView: CoverView()) cover.level = .statusBar cover.sharingType = .none cover.backgroundColor = .clear cover.ignoresMouseEvents = true cover.isReleasedWhenClosed = false cover.collectionBehavior = [.canJoinAllSpaces, .stationary] cover.title = "Screen Cover" cover.orderFront(self) } if mouseMonitor == nil { mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved]) { _ in self.updateMask() } } if mouseMonitorL == nil { mouseMonitorL = NSEvent.addLocalMonitorForEvents(matching: [.mouseMoved]) { event in self.updateMask() return event } } } func stopMouseMonitor() { DispatchQueue.main.async { for w in NSApp.windows.filter({ $0.title == "Screen Cover" }) { w.close() } } if let monitor = mouseMonitor { NSEvent.removeMonitor(monitor) mouseMonitor = nil } if let monitor = mouseMonitorL { NSEvent.removeMonitor(monitor) mouseMonitorL = nil } } func updateMask() { guard let targetWindow = getWindowUnderMouse() else { mask?.close() targetWindowID = nil return } if let app = targetWindow["kCGWindowOwnerName"] as? String, app != Bundle.main.appName, let windowID = targetWindow["kCGWindowNumber"] as? Int, targetWindowID != windowID { mask?.close() targetWindowID = windowID createMaskWindow(window: targetWindow) } } func createMaskWindow(window: [String: Any]) { guard let windowID = targetWindowID, let frame = getCGWindowFrame(window: window) else { return } let app = window["kCGWindowOwnerName"] as? String ?? "" let title = window["kCGWindowName"] as? String ?? "" mask = EscPanel(contentRect: CGRectTransform(cgRect: frame), styleMask: [.nonactivatingPanel, .fullSizeContentView], backing: .buffered, defer: false) let contentView = NSHostingView(rootView: HighlightMask(app: app, title: title, windowID: windowID)) mask?.contentView = contentView mask?.title = "Mask Window" mask?.hasShadow = false mask?.sharingType = .none mask?.backgroundColor = .clear mask?.titleVisibility = .hidden mask?.isMovableByWindowBackground = false mask?.isReleasedWhenClosed = false mask?.collectionBehavior = [.canJoinAllSpaces, .transient] mask?.setFrame(CGRectTransform(cgRect: frame), display: true) mask?.order(.above, relativeTo: windowID) mask?.makeKey() } func getWindowUnderMouse() -> [String: Any]? { let mousePosition = NSEvent.mouseLocation guard let windowList = getAllCGWindows() else { return nil } for window in windowList { guard let bounds = getCGWindowFrame(window: window) else { continue } if CGRectTransform(cgRect: bounds).contains(mousePosition) { return window } } return nil } func getAllCGWindows() -> [[String: Any]]? { guard var windowList = CGWindowListCopyWindowInfo([.excludeDesktopElements,.optionOnScreenOnly], kCGNullWindowID) as? [[String: Any]] else { return nil } windowList = windowList.filter({ !["SystemUIServer", "Window Server"].contains($0["kCGWindowOwnerName"] as? String) && $0["kCGWindowAlpha"] as? NSNumber != 0 && $0["kCGWindowLayer"] as? NSNumber == 0 }) return windowList } func getSCWindowWithID(_ windowID: UInt32?) -> SCWindow? { guard let windowID else { return nil } _ = SCContext.updateAvailableContentSync() let windows = SCContext.getWindows() return windows.first(where: { $0.windowID == windowID }) } func getCGWindowFrame(window: [String: Any]) -> CGRect? { guard let boundsDict = window["kCGWindowBounds"] as? [String: CGFloat] else { return nil } let bounds = CGRect( x: boundsDict["X"] ?? 0, y: boundsDict["Y"] ?? 0, width: boundsDict["Width"] ?? 0, height: boundsDict["Height"] ?? 0 ) return bounds } func CGRectTransform(cgRect: CGRect) -> NSRect { let x = cgRect.origin.x let y = cgRect.origin.y let w = cgRect.width let h = cgRect.height if let main = NSScreen.screens.first(where: { $0.isMainScreen }) { return NSRect(x: x, y: main.frame.height - y - h, width: w, height: h) } return cgRect } } class EscPanel: NSPanel { override func cancelOperation(_ sender: Any?) { self.close() WindowHighlighter.shared.stopMouseMonitor() } override var canBecomeKey: Bool { return true } } func CGRectTransform(cgRect: CGRect) -> NSRect { let x = cgRect.origin.x let y = cgRect.origin.y let w = cgRect.width let h = cgRect.height if let main = NSScreen.screens.first(where: { $0.isMainScreen }) { return NSRect(x: x, y: main.frame.height - y - h, width: w, height: h) } return cgRect } extension View { func onPressGesture(perform: @escaping () -> Void) -> some View { self.gesture( DragGesture(minimumDistance: 0) .onChanged { _ in perform() } ) } } ================================================ FILE: QuickRecorder/ViewModel/AppBlockSelector.swift ================================================ // // BundleSelector.swift // QuickRecorder // // Created by apple on 2024/4/28. // import SwiftUI import Foundation struct BundleSelector: View { @State private var Bundles = [AppInfo]() @State private var isShowingFilePicker = false var body: some View { ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { List(Bundles, id: \.self) { item in HStack{ Image(systemName: "minus.circle.fill") .font(.system(size: 12)) .foregroundStyle(.red) .onTapGesture { if let index = Bundles.firstIndex(of: item) { _ = withAnimation { Bundles.remove(at: index) } } } Text(item.displayName).font(.system(size: 12)) } } Button(action: { self.isShowingFilePicker = true }) { Image(systemName: "plus.square.fill") .font(.system(size: 20)) .foregroundStyle(.secondary) }.buttonStyle(.plain).offset(y: -1) } .onAppear { if let savedData = ud.data(forKey: "hiddenApps"), let decodedApps = try? JSONDecoder().decode([AppInfo].self, from: savedData) { Bundles = decodedApps } } .onChange(of: Bundles) { bundles in if let encodedData = try? JSONEncoder().encode(bundles) { ud.set(encodedData, forKey: "hiddenApps") } } .fileImporter(isPresented: $isShowingFilePicker, allowedContentTypes: [.application]) { result in do { guard let appID = try Bundle(url: result.get())?.bundleIdentifier else { return } guard let displayName = try Bundle(url: result.get())?.fileName else { return } let app = AppInfo(bundleID: appID, displayName: displayName) if !self.Bundles.contains(app) { withAnimation { Bundles.append(app) } } } catch { print("File selection failed: \(error.localizedDescription)") } } } } struct AppInfo: Hashable, Codable { let bundleID: String let displayName: String } extension Bundle { var bundleName: String? { return object(forInfoDictionaryKey: "CFBundleName") as? String } var fileName: String { return self.bundleURL.lastPathComponent } } ================================================ FILE: QuickRecorder/ViewModel/AppSelector.swift ================================================ // // AppSelector.swift // QuickRecorder // // Created by apple on 2024/4/16. // import SwiftUI import Combine import ScreenCaptureKit struct AppSelector: View { @StateObject var viewModel = AppSelectorViewModel() @State private var selected = [SCRunningApplication]() @State private var display: SCDisplay! @State private var selectedTab = 0 @State private var isPopoverShowing = false @State private var autoStop = 0 var appDelegate = AppDelegate.shared var body: some View { ZStack { VStack(spacing: 15) { if #available(macOS 15, *) { Text("Please select the App(s) to record").offset(y: 12) } else { Text("Please select the App(s) to record") } TabView(selection: $selectedTab) { let allApps = viewModel.allApps.sorted(by: { $0.key.displayID < $1.key.displayID }) ForEach(allApps, id: \.key) { element in let (screen, apps) = element let index = allApps.firstIndex(where: { $0.key == screen }) ?? 0 ScrollView(.vertical) { VStack(spacing: 8) { ForEach(0.. ResizeHandle { guard let rect = selectionRect else { return .none } for handle in ResizeHandle.allCases { if let controlPoint = controlPointForHandle(handle, inRect: rect), NSRect(origin: controlPoint, size: CGSize(width: controlPointSize, height: controlPointSize)).contains(point) { return handle } } return .none } func controlPointForHandle(_ handle: ResizeHandle, inRect rect: NSRect) -> NSPoint? { switch handle { case .topLeft: return NSPoint(x: rect.minX - controlPointSize / 2 - 1, y: rect.maxY - controlPointSize / 2 + 1) case .top: return NSPoint(x: rect.midX - controlPointSize / 2, y: rect.maxY - controlPointSize / 2 + 1) case .topRight: return NSPoint(x: rect.maxX - controlPointSize / 2 + 1, y: rect.maxY - controlPointSize / 2 + 1) case .right: return NSPoint(x: rect.maxX - controlPointSize / 2 + 1, y: rect.midY - controlPointSize / 2) case .bottomRight: return NSPoint(x: rect.maxX - controlPointSize / 2 + 1, y: rect.minY - controlPointSize / 2 - 1) case .bottom: return NSPoint(x: rect.midX - controlPointSize / 2, y: rect.minY - controlPointSize / 2 - 1) case .bottomLeft: return NSPoint(x: rect.minX - controlPointSize / 2 - 1, y: rect.minY - controlPointSize / 2 - 1) case .left: return NSPoint(x: rect.minX - controlPointSize / 2 - 1, y: rect.midY - controlPointSize / 2) case .none: return nil } } override func mouseDown(with event: NSEvent) { let location = convert(event.locationInWindow, from: nil) initialLocation = location lastMouseLocation = location activeHandle = handleForPoint(location) if let rect = selectionRect, NSPointInRect(location, rect) { dragIng = true } AppDelegate.shared.isResizing = true } override func mouseDragged(with event: NSEvent) { guard var initialLocation = initialLocation else { return } let currentLocation = convert(event.locationInWindow, from: nil) if activeHandle != .none { // Calculate new rectangle size and position var newRect = selectionRect ?? CGRect.zero // Get last mouse location let lastLocation = lastMouseLocation ?? currentLocation let deltaX = currentLocation.x - lastLocation.x let deltaY = currentLocation.y - lastLocation.y switch activeHandle { case .topLeft: newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) newRect.size.width = max(20, newRect.size.width - deltaX) newRect.size.height = max(20, newRect.size.height + deltaY) case .top: newRect.size.height = max(20, newRect.size.height + deltaY) case .topRight: newRect.size.width = max(20, newRect.size.width + deltaX) newRect.size.height = max(20, newRect.size.height + deltaY) case .right: newRect.size.width = max(20, newRect.size.width + deltaX) case .bottomRight: newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) newRect.size.width = max(20, newRect.size.width + deltaX) newRect.size.height = max(20, newRect.size.height - deltaY) case .bottom: newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) newRect.size.height = max(20, newRect.size.height - deltaY) case .bottomLeft: newRect.origin.y = min(newRect.origin.y + newRect.size.height - 20, newRect.origin.y + deltaY) newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) newRect.size.width = max(20, newRect.size.width - deltaX) newRect.size.height = max(20, newRect.size.height - deltaY) case .left: newRect.origin.x = min(newRect.origin.x + newRect.size.width - 20, newRect.origin.x + deltaX) newRect.size.width = max(20, newRect.size.width - deltaX) default: break } self.selectionRect = newRect initialLocation = currentLocation // Update initial location for continuous dragging lastMouseLocation = currentLocation // Update last mouse location areaWidth = Int(selectionRect!.width) areaHeight = Int(selectionRect!.height) } else { if dragIng { dragIng = true // 计算移动偏移量 let deltaX = currentLocation.x - initialLocation.x let deltaY = currentLocation.y - initialLocation.y // 更新矩形位置 let x = self.selectionRect?.origin.x let y = self.selectionRect?.origin.y let w = self.selectionRect?.size.width let h = self.selectionRect?.size.height self.selectionRect?.origin.x = min(max(0.0, x! + deltaX), self.frame.width - w!) self.selectionRect?.origin.y = min(max(0.0, y! + deltaY), self.frame.height - h!) initialLocation = currentLocation } else { //dragIng = false // 创建新矩形 guard let maxFrame = maxFrame else { return } let origin = NSPoint(x: max(maxFrame.origin.x, min(initialLocation.x, currentLocation.x)), y: max(maxFrame.origin.y, min(initialLocation.y, currentLocation.y))) var maxH = abs(currentLocation.y - initialLocation.y) var maxW = abs(currentLocation.x - initialLocation.x) if currentLocation.y < maxFrame.origin.y { maxH = initialLocation.y } if currentLocation.x < maxFrame.origin.x { maxW = initialLocation.x } let size = NSSize(width: maxW, height: maxH) self.selectionRect = NSIntersectionRect(maxFrame, NSRect(origin: origin, size: size)) areaWidth = Int(selectionRect!.width) areaHeight = Int(selectionRect!.height) //initialLocation = currentLocation } self.initialLocation = initialLocation } lastMouseLocation = currentLocation } override func mouseUp(with event: NSEvent) { initialLocation = nil activeHandle = .none dragIng = false AppDelegate.shared.isResizing = false if let rect = selectionRect { SCContext.screenArea = rect } } } class ScreenshotWindow: NSPanel { init(contentRect: NSRect, backing bufferingType: NSWindow.BackingStoreType, defer flag: Bool, size: NSSize, force: Bool = false) { let overlayView = ScreenshotOverlayView(frame: contentRect, size:size, force: force) super.init(contentRect: contentRect, styleMask: [.borderless, .nonactivatingPanel], backing: bufferingType, defer: flag) self.isOpaque = false self.hasShadow = false self.level = .statusBar self.backgroundColor = NSColor.clear self.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] self.isReleasedWhenClosed = false self.contentView = overlayView if keyMonitor != nil { return } keyMonitor = NSEvent.addLocalMonitorForEvents(matching: NSEvent.EventTypeMask.keyDown, handler: myKeyDownEvent) } required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } func myKeyDownEvent(event: NSEvent) -> NSEvent? { if event.keyCode == 53 && !event.isARepeat { self.close() for w in NSApp.windows.filter({ $0.title == "Start Recording".local }) { w.close() } AppDelegate.shared.stopGlobalMouseMonitor() if let monitor = keyMonitor { NSEvent.removeMonitor(monitor) keyMonitor = nil } return nil } return event } } enum ResizeHandle: CaseIterable { case none case topLeft, top, topRight, right, bottomRight, bottom, bottomLeft, left static var allCases: [ResizeHandle] { return [.none, .topLeft, .top, .topRight, .right, .bottomRight, .bottom, .bottomLeft, .left] } } ================================================ FILE: QuickRecorder/ViewModel/CameraOverlayer.swift ================================================ // // CameraOverlayer.swift // QuickRecorder // // Created by apple on 2024/4/29. // import SwiftUI import AppKit import Foundation import AVFoundation extension AppDelegate { func startCameraOverlayer(size: NSSize = NSSize(width: 200, height: 200)){ guard let screen = SCContext.getScreenWithMouse() else { return } camWindow.contentView = NSHostingView(rootView: SwiftCameraView(type: .camera)) let frame = NSRect(x: (screen.visibleFrame.width-size.width)/2+screen.frame.minX, y: (screen.visibleFrame.height-size.height)/2+screen.frame.minY, width: size.width, height: size.height) camWindow.setFrame(frame, display: true) //camWindow.setFrameOrigin(NSPoint(x: screen.visibleFrame.width/2-100, y: screen.visibleFrame.height/2-100)) camWindow.contentView?.wantsLayer = true camWindow.contentView?.layer?.cornerRadius = 5 camWindow.contentView?.layer?.masksToBounds = true camWindow.orderFront(self) } } struct CameraView: NSViewRepresentable { var type: StreamType! func makeNSView(context: Context) -> CameraNSView { let cameraView = CameraNSView(frame: .zero, type: type) return cameraView } func updateNSView(_ nsView: CameraNSView, context: Context) { // Update the view } } class CameraNSView: NSView { let type: StreamType var session = SCContext.captureSession var previewLayer: AVCaptureVideoPreviewLayer? = nil init(frame frameRect: NSRect, type: StreamType) { self.type = type super.init(frame: frameRect) wantsLayer = true setupCaptureSession() } required init?(coder decoder: NSCoder) { // 如果您的类型不是一个可选类型,您可以将其设置为一个默认值 self.type = .camera super.init(coder: decoder) wantsLayer = true setupCaptureSession() } private func setupCaptureSession() { if type == .idevice { session = SCContext.previewSession } guard let session = session else { return } previewLayer = AVCaptureVideoPreviewLayer(session: session) previewLayer!.frame = bounds if type == .idevice { previewLayer!.videoGravity = AVLayerVideoGravity.resizeAspect } if type == .camera { previewLayer!.videoGravity = AVLayerVideoGravity.resizeAspectFill previewLayer!.setAffineTransform(CGAffineTransform(scaleX: -1, y: 1)) } layer?.addSublayer(previewLayer!) } override func layout() { super.layout() previewLayer?.frame = bounds } } struct SwiftCameraView: View { var type: StreamType! @State private var hover = false @State private var isFlipped = false var body: some View { GeometryReader { geometry in ZStack { if type == .idevice { Color.black Text("Please unlock!") .foregroundStyle(.white) } ZStack(alignment: Alignment(horizontal: .trailing, vertical: .bottom)) { CameraView(type: type) .rotation3DEffect(.degrees(isFlipped ? 180 : 0), axis: (x: 0, y: 1, z: 0)) Button(action: { if type == .idevice { for w in NSApp.windows.filter({ $0.title == "iDevice Overlayer".local }) { w.close() } } else { isFlipped.toggle() } }, label: { ZStack { Circle().frame(width: 30) .foregroundStyle(hover ? .blue : .gray) if type == .idevice { Image(systemName: "xmark") .foregroundStyle(.white) } else { Image(systemName: "arrow.left.and.right.righttriangle.left.righttriangle.right.fill") .foregroundStyle(.white) .offset(y: -1) } } .opacity(hover ? 0.8 : 0.2) .onHover{ hovering in hover = hovering } }).buttonStyle(.plain).padding(10) }.frame(width: geometry.size.width, height: geometry.size.height) if SCContext.streamType == .window { Text("Unable to use camera overlayer when recording a single window!".local + (isMacOS14 ? " Please use \"Presenter Overlay\"".local : "") ) .padding() .colorInvert() .background(.secondary) } } } .frame(minWidth: 100, minHeight: 100) .onHover { hovering in hideMousePointer = hovering hideScreenMagnifier = hovering } } } struct CameraPopoverView: View { var closePopover: () -> Void @State private var cameras = SCContext.getCameras() @State private var devices = SCContext.getiDevice() @State private var hoverIndex = -1 @State private var hoverIndex2 = -1 @State private var disabled = false //@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate var appDelegate = AppDelegate.shared var body: some View { VStack( alignment: .leading, spacing: 0) { if cameras.count < 1 { HStack { ZStack { Circle().frame(width: 26) .foregroundStyle(.primary) .opacity(0.2) Image(systemName:"video.slash.fill") .foregroundStyle(.primary) .font(.system(size: 12)) }.padding(.leading, 9) Text("No Cameras Found!".local) .padding(.vertical, 8).padding(.trailing, 10) }.frame(maxWidth: .infinity) } ForEach(cameras.indices, id: \.self) { index in Button(action: { closePopover() if SCContext.recordCam == cameras[index].localizedName { SCContext.recordCam = "" appDelegate.closeCamera() return } SCContext.recordCam = cameras[index].localizedName appDelegate.closeCamera() appDelegate.recordingCamera(with: cameras[index]) }, label: { HStack { ZStack { Circle().frame(width: 26) .foregroundStyle(SCContext.recordCam == cameras[index].localizedName ? .blue : .primary) .opacity(SCContext.recordCam == cameras[index].localizedName ? 1.0 : 0.2) Image(systemName: "video.fill") .foregroundStyle(SCContext.recordCam == cameras[index].localizedName ? .white : .primary) .font(.system(size: 12)) }.padding(.leading, 9) Text(cameras[index].localizedName) .padding(.vertical, 8).padding(.trailing, 10) Spacer() } .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 5) .foregroundStyle(.primary) .opacity(hoverIndex == index ? 0.2 : 0.0) ) .onHover{ hovering in if hoverIndex != index { hoverIndex = index } if !hovering { hoverIndex = -1 } } }).buttonStyle(.plain) } if SCContext.streamType != .window { if !devices.isEmpty { Divider().padding(.vertical, 4) } ForEach(devices.indices, id: \.self) { index in Button(action: { closePopover() if SCContext.recordDevice == devices[index].localizedName { SCContext.recordDevice = "" AVOutputClass.shared.closePreview() return } SCContext.recordDevice = devices[index].localizedName AVOutputClass.shared.closePreview() DispatchQueue.global().async { AVOutputClass.shared.startRecording(with: devices[index], mute: true, didOutput: false) } }, label: { HStack { ZStack { Circle().frame(width: 26) .foregroundStyle(SCContext.recordDevice == devices[index].localizedName ? .blue : .primary) .opacity(SCContext.recordDevice == devices[index].localizedName ? 1.0 : 0.2) Image(systemName:"apple.logo") .foregroundStyle(SCContext.recordDevice == devices[index].localizedName ? .white : .primary) .font(.system(size: 12)) }.padding(.leading, 9) Text(devices[index].localizedName) .padding(.vertical, 8).padding(.trailing, 10) Spacer() } .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: 5) .foregroundStyle(.primary) .opacity(hoverIndex2 == index ? 0.2 : 0.0) ) .onHover{ hovering in if hoverIndex2 != index { hoverIndex2 = index } if !hovering { hoverIndex2 = -1 } } }).buttonStyle(.plain) } } }.padding(5) } } ================================================ FILE: QuickRecorder/ViewModel/ContentView.swift ================================================ // // ContentView.swift // QuickRecorder // // Created by apple on 2024/4/16. // import SwiftUI import AVFoundation import ScreenCaptureKit struct ContentView: View { var fromStatusBar = false @State private var window: NSWindow? @State private var xmarkGlowing = false @State private var infoGlowing = false @State private var micGlowing = false @State private var isPopoverShowing = false @State private var isPopoverShowing2 = false @State private var micList = SCContext.getMicrophone() @AppStorage("enableAEC") private var enableAEC: Bool = false @AppStorage("recordMic") private var recordMic: Bool = false @AppStorage("micDevice") private var micDevice: String = "default" @AppStorage("showOnDock") private var showOnDock: Bool = true @AppStorage("showMenubar") private var showMenubar: Bool = false var appDelegate = AppDelegate.shared var body: some View { ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ZStack { ZStack { if !fromStatusBar { Color.clear .background(.ultraThinMaterial) .environment(\.controlActiveState, .active) } ZStack { if isTodayChristmas() && isAllowChristmas() { let images = ["snowflake1", "snowflake2", "snowflake3", "christmasTree1", "christmasTree2"] SurpriseView(snowflakes: images, width: (!showOnDock && !showMenubar) ? 1055 : 930, height: 100) } else if isChineseNewYear() && isAllowChineseNewYear() { let images = ["fuzi1", "fuzi2", "fuzi3", "hongbao1", "hongbao3"] SurpriseView(snowflakes: images, width: (!showOnDock && !showMenubar) ? 1055 : 930, height: 100) } }.opacity(0.5) }.cornerRadius(14) HStack { if !fromStatusBar { Spacer() } if #available(macOS 13, *) { ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) { Button(action: { if let display = SCContext.getSCDisplayWithMouse() { closeMainWindow() appDelegate.createCountdownPanel(screen: display) { AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) } } }, label: { SelectorView(title: "System Audio".local, symbol: "waveform").cornerRadius(8) }).buttonStyle(.plain) Button {} label: { HStack(spacing: -2) { Button { recordMic.toggle() } label: { ZStack { Image(systemName: "square.fill") .font(.system(size: 15, weight: .medium)) .foregroundColor(.primary) .colorInvert() .opacity(0.2) Image(systemName: recordMic ? "checkmark.square" : "square") .font(.system(size: 16, weight: .medium)) } } .buttonStyle(.plain) .onChange(of: recordMic) { _ in Task { await SCContext.performMicCheck() }} .onAppear{ if micList.isEmpty { recordMic = false } } .disabled(micList.isEmpty) if micDevice != "default" && enableAEC && recordMic{ Button { let alert = createAlert( title: "Compatibility Warning".local, message: "The \"Acoustic Echo Cancellation\" is enabled, but it won't work on now.\n\nIf you need to use a specific input with AEC, set it to \"Default\" and select the device you want in System Preferences.\n\nOr you can start recording without AEC.".local, button1: "OK".local, button2: "System Preferences".local) if alert.runModal() == .alertSecondButtonReturn { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.sound?input")!) } } label: { ZStack { Image(systemName: "circle.fill").font(.system(size: 15)) Image(systemName: "exclamationmark") .font(.system(size: 11.5, weight: .black)) .foregroundColor(.black) .blendMode(.destinationOut) }.compositingGroup() } .buttonStyle(.plain) .frame(width: 24).fixedSize() .padding(.leading, 1) } else { Image(systemName: "mic.fill") .font(.system(size: 13, weight: .bold)) .foregroundColor(recordMic ? .primary : .secondary) .frame(width: 24) .padding(.leading, 1) } if #available(macOS 14, *) { Picker("", selection: $micDevice) { Text("Default".local).tag("default") ForEach(micList, id: \.self) { device in Text(device.localizedName).tag(device.localizedName) } }.frame(width: 90) .background( ZStack { Color.primary .opacity(0.1) .cornerRadius(4) .padding(.vertical, -1) .padding(.horizontal, 3) .padding(.trailing, -16) Image(systemName: "chevron.up.chevron.down") .offset(x: 50) } ) .disabled(!recordMic) .padding(.leading, -10) .frame(width: 99) .onAppear{ let list = micList.map({ $0.localizedName }) if !list.contains(micDevice) { micDevice = "default" } } } else { Spacer().frame(width: 6) Picker("", selection: $micDevice) { Text("Default".local).tag("default") ForEach(micList, id: \.self) { device in Text(device.localizedName).tag(device.localizedName) } } .disabled(!recordMic) .padding(.leading, -7.5) .frame(width: 100) .onAppear{ let list = micList.map({ $0.localizedName }) if !list.contains(micDevice) { micDevice = "default" } } } }.padding(.leading, -5) }.buttonStyle(.plain) .scaleEffect(0.69) .padding(.bottom, 4) .frame(width: 110) .background(.primary.opacity(0.00001)) } Divider().frame(height: 70) } Button(action: { closeMainWindow() appDelegate.createNewWindow(view: ScreenSelector(), title: "Screen Selector".local) }, label: { SelectorView(title: "Screen".local, symbol: "tv.inset.filled").cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() SCContext.updateAvailableContent { DispatchQueue.main.async { appDelegate.showAreaSelector(size: NSSize(width: 600, height: 450)) var currentDisplay = SCContext.getSCDisplayWithMouse() mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .rightMouseDown, .leftMouseDown, .otherMouseDown]) { event in let display = SCContext.getSCDisplayWithMouse() if display != currentDisplay { currentDisplay = display closeAllWindow() appDelegate.showAreaSelector(size: NSSize(width: 600, height: 450)) } } } } }, label: { SelectorView(title: "Screen Area".local, symbol: "viewfinder").cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.createNewWindow(view: AppSelector(), title: "App Selector".local) }, label: { SelectorView(title: "Application".local, symbol: "app", symbolSize: 38, overlayer: "App") .cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.createNewWindow(view: WinSelector(), title: "Window Selector".local) }, label: { SelectorView(title: "Window".local, symbol: "macwindow").cornerRadius(8) }).buttonStyle(.plain) /*Divider().frame(height: 70) Button(action: { isPopoverShowing2 = true }, label: { SelectorView(title: "Camera".local, symbol: "camera").cornerRadius(8) }) .buttonStyle(.plain) .popover(isPresented: $isPopoverShowing2, arrowEdge: .bottom) { CameraPopoverView(closePopover: { isPopoverShowing2 = false }) }*/ Divider().frame(height: 70) Button(action: { isPopoverShowing = true }, label: { SelectorView(title: "Mobile Device".local, symbol: "apps.ipad").cornerRadius(8) }) .buttonStyle(.plain) .popover(isPresented: $isPopoverShowing, arrowEdge: .bottom) { iDevicePopoverView(closePopover: { isPopoverShowing = false }) } Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.openSettingPanel() }, label: { SelectorView(title: "Preferences".local, symbol: "gearshape").cornerRadius(8) }).buttonStyle(.plain) if fromStatusBar || (!showOnDock && !showMenubar) { Divider().frame(height: 70) Button(action: { NSApp.terminate(self) }, label: { SelectorView(title: "Quit".local, symbol: "xmark.circle") .cornerRadius(8) .foregroundStyle(.darkMyRed) }).buttonStyle(.plain) } if !fromStatusBar { Spacer() } }.padding(.vertical, 10).padding(.horizontal, fromStatusBar ? 10 : 20) } if !fromStatusBar { Button(action: { closeMainWindow() }, label: { Image(systemName: "x.circle") .font(.system(size: 13, weight: .bold)) .opacity(xmarkGlowing ? 1.0 : 0.4) .foregroundStyle(.secondary) .onHover{ hovering in xmarkGlowing = hovering } }) .buttonStyle(.plain) .padding([.horizontal, .top], 7) } }.focusable(false) } } struct SelectorView: View { var title = "No Title".local var symbol = "app" var symbolSize: CGFloat = 36 var overlayer = "" @State private var backgroundOpacity = 0.0001 var body: some View { VStack(spacing: 6) { Text(title) .opacity(0.95) .font(.system(size: 12)) .offset(y: title == "System Audio".local ? -3.5 : 0) ZStack { if title == "System Audio".local { Image(systemName: symbol) .opacity(0.95) .offset(y: -9.5) .font(.system(size: 26, weight: .bold)) } else { Image(systemName: symbol) .opacity(0.95) .font(.system(size: symbolSize)) .frame(height: 40) } Text(overlayer) .fontWeight(.bold) .opacity(0.95) .font(.system(size: 11)) } } .frame(width: 110, height: 80) .onHover{ hovering in backgroundOpacity = hovering ? 0.2 : 0.0001 } .background( .primary.opacity(backgroundOpacity) ) } } struct CountdownView: View { @State var countdownValue: Int = 00 @State private var timer: Timer? var atEnd: () -> Void var body: some View { ZStack { Color.mypurple.environment(\.colorScheme, .dark) Text("\(countdownValue)") .font(.system(size: 72)) .foregroundColor(.white) .offset(y: -10) Button(action: { timer?.invalidate() for w in NSApp.windows.filter({ $0.title == "Countdown Panel".local || $0.title == "Area Overlayer".local }) { w.close() } }, label: { ZStack { Color.white.opacity(0.2) Text("Cancel").foregroundColor(.white) }.frame(width: 120, height: 24) }) .buttonStyle(.plain) .padding(.top, 96) } .frame(width: 120, height: 120) .cornerRadius(10) .onAppear{ timer?.invalidate() timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { timer in if countdownValue > 1 { countdownValue -= 1 } else { timer.invalidate() if let w = NSApp.windows.first(where: { $0.title == "Countdown Panel".local }) { w.close() } atEnd() } } } } } extension AppDelegate { func showAreaSelector(size: NSSize, noPanel: Bool = false) { guard let scDisplay = SCContext.getSCDisplayWithMouse() else { return } guard let screen = scDisplay.nsScreen else { return } let screenshotWindow = ScreenshotWindow(contentRect: screen.frame, backing: .buffered, defer: false, size: size, force: noPanel) screenshotWindow.title = "Area Selector".local //screenshotWindow.orderFront(self) screenshotWindow.orderFrontRegardless() if !noPanel { let wX = (screen.frame.width - 790) / 2 + screen.frame.minX let wY = screen.visibleFrame.minY + 80 let contentView = NSHostingView(rootView: AreaSelector(screen: scDisplay)) contentView.frame = NSRect(x: wX, y: wY, width: 790, height: 90) contentView.focusRingType = .none let areaPanel = NSPanel(contentRect: contentView.frame, styleMask: [.fullSizeContentView, .nonactivatingPanel], backing: .buffered, defer: false) areaPanel.collectionBehavior = [.canJoinAllSpaces] areaPanel.setFrame(contentView.frame, display: true) areaPanel.level = .screenSaver areaPanel.title = "Start Recording".local areaPanel.contentView = contentView areaPanel.backgroundColor = .clear areaPanel.titleVisibility = .hidden areaPanel.isReleasedWhenClosed = false areaPanel.titlebarAppearsTransparent = true areaPanel.isMovableByWindowBackground = true //areaPanel.setFrameOrigin(NSPoint(x: wX, y: wY)) areaPanel.orderFront(self) } } func createCountdownPanel(screen: SCDisplay, action: @escaping () -> Void) { guard let screen = screen.nsScreen else { return } let countdown = ud.integer(forKey: "countdown") if countdown == 0 { action() } else { let wX = (screen.frame.width - 120) / 2 + screen.frame.minX let wY = (screen.frame.height - 120) / 2 + screen.frame.minY let frame = NSRect(x: wX, y: wY, width: 120, height: 120) let contentView = NSHostingView(rootView: CountdownView(countdownValue: countdown, atEnd: action)) contentView.frame = frame countdownPanel.contentView = contentView countdownPanel.setFrame(frame, display: true) countdownPanel.makeKeyAndOrderFront(self) } } func createNewWindow(view: some View, title: String, random: Bool = false, only: Bool = true) { guard let screen = SCContext.getScreenWithMouse() else { return } if only { closeAllWindow() } var seed = 0.0 if random { seed = CGFloat(Int(arc4random_uniform(401)) - 200) } let wX = (screen.frame.width - 780) / 2 + seed + screen.frame.minX let wY = (screen.frame.height - 555) / 2 + 100 + seed + screen.frame.minY let contentView = NSHostingView(rootView: view) contentView.frame = NSRect(x: wX, y: wY, width: 780, height: 555) let window = NSWindow(contentRect: contentView.frame, styleMask: [.titled, .closable, .miniaturizable], backing: .buffered, defer: false) window.title = title window.contentView = contentView window.titleVisibility = .hidden window.titlebarAppearsTransparent = true window.isMovableByWindowBackground = true window.isReleasedWhenClosed = false window.makeKeyAndOrderFront(self) window.orderFrontRegardless() } } extension View { func needScale() -> some View { if #available(macOS 14, *) { return self.scaleEffect(0.8).padding(.leading, -4) } else { return self } } } /*#Preview { ContentView() } */ ================================================ FILE: QuickRecorder/ViewModel/ContentViewNew.swift ================================================ // // ContentView.swift // QuickRecorder // // Created by apple on 2024/4/16. // import SwiftUI import AVFoundation import ScreenCaptureKit struct ContentViewNew: View { @State private var window: NSWindow? @State private var xmarkGlowing = false @State private var infoGlowing = false @State private var micGlowing = false @State private var isPopoverShowing = false @State private var micList = SCContext.getMicrophone() @AppStorage("enableAEC") private var enableAEC: Bool = false @AppStorage("recordMic") private var recordMic: Bool = false @AppStorage("micDevice") private var micDevice: String = "default" @AppStorage("showOnDock") private var showOnDock: Bool = true @AppStorage("showMenubar") private var showMenubar: Bool = false var appDelegate = AppDelegate.shared var body: some View { ZStack { ZStack { if isTodayChristmas() && isAllowChristmas() { let images = ["snowflake1", "snowflake2", "snowflake3", "christmasTree1", "christmasTree2"] SurpriseView(snowflakes: images, width: 520, height: 200, velocity: 16, lifetime: 30, alphaSpeed: -0.05) } else if isChineseNewYear() && isAllowChineseNewYear() { let images = ["fuzi1", "fuzi2", "fuzi3", "hongbao1", "hongbao2", "hongbao3", "bianpao1", "bianpao2", "bianpao3"] SurpriseView(snowflakes: images, width: 520, height: 200, velocity: 16, lifetime: 30, alphaSpeed: -0.05) } }.opacity(0.5) VStack { HStack { Button(action: { closeMainWindow() appDelegate.createNewWindow(view: ScreenSelector(), title: "Screen Selector".local) }, label: { SelectorView(title: "Screen".local, symbol: "tv.inset.filled").cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() SCContext.updateAvailableContent { DispatchQueue.main.async { appDelegate.showAreaSelector(size: NSSize(width: 600, height: 450)) var currentDisplay = SCContext.getSCDisplayWithMouse() mouseMonitor = NSEvent.addGlobalMonitorForEvents(matching: [.mouseMoved, .rightMouseDown, .leftMouseDown, .otherMouseDown]) { event in let display = SCContext.getSCDisplayWithMouse() if display != currentDisplay { currentDisplay = display closeAllWindow() appDelegate.showAreaSelector(size: NSSize(width: 600, height: 450)) } } } } }, label: { SelectorView(title: "Screen Area".local, symbol: "viewfinder").cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.createNewWindow(view: AppSelector(), title: "App Selector".local) }, label: { SelectorView(title: "Application".local, symbol: "app", overlayer: "App") .cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.createNewWindow(view: WinSelector(), title: "Window Selector".local) }, label: { SelectorView(title: "Window".local, symbol: "macwindow").cornerRadius(8) }).buttonStyle(.plain) } HStack(spacing: 27) { VStack { Divider().frame(width: 100) } VStack { Divider().frame(width: 100) } VStack { Divider().frame(width: 100) } VStack { Divider().frame(width: 100) } } HStack { ZStack(alignment: Alignment(horizontal: .center, vertical: .bottom)) { Button(action: { if let display = SCContext.getSCDisplayWithMouse() { closeMainWindow() appDelegate.createCountdownPanel(screen: display) { AppDelegate.shared.prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil) } } }, label: { SelectorView(title: "System Audio".local, symbol: "waveform").cornerRadius(8) }).buttonStyle(.plain) Button {} label: { HStack(spacing: -2) { Button { recordMic.toggle() } label: { ZStack { Image(systemName: "square.fill") .font(.system(size: 15, weight: .medium)) .foregroundColor(.primary) .colorInvert() .opacity(0.2) Image(systemName: recordMic ? "checkmark.square" : "square") .font(.system(size: 16, weight: .medium)) } } .buttonStyle(.plain) .onChange(of: recordMic) { _ in Task { await SCContext.performMicCheck() }} if micDevice != "default" && enableAEC && recordMic{ Button { let alert = createAlert( title: "Compatibility Warning".local, message: "The \"Acoustic Echo Cancellation\" is enabled, but it won't work on now.\n\nIf you need to use a specific input with AEC, set it to \"Default\" and select the device you want in System Preferences.\n\nOr you can start recording without AEC.".local, button1: "OK".local, button2: "System Preferences".local) if alert.runModal() == .alertSecondButtonReturn { NSWorkspace.shared.open(URL(string: "x-apple.systempreferences:com.apple.preference.sound?input")!) } } label: { ZStack { Image(systemName: "circle.fill").font(.system(size: 15)) Image(systemName: "exclamationmark") .font(.system(size: 11.5, weight: .black)) .foregroundColor(.black) .blendMode(.destinationOut) }.compositingGroup() } .buttonStyle(.plain) .frame(width: 24).fixedSize() .padding(.leading, 1) } else { Image(systemName: "mic.fill") .font(.system(size: 13, weight: .bold)) .foregroundColor(recordMic ? .primary : .secondary) .frame(width: 24) .padding(.leading, 1) } if #available(macOS 14, *) { Picker("", selection: $micDevice) { Text("Default".local).tag("default") ForEach(micList, id: \.self) { device in Text(device.localizedName).tag(device.localizedName) } }.frame(width: 90) .background( ZStack { Color.primary .opacity(0.1) .cornerRadius(4) .padding(.vertical, -1) .padding(.horizontal, 3) .padding(.trailing, -16) Image(systemName: "chevron.up.chevron.down") .offset(x: 50) } ) .disabled(!recordMic) .padding(.leading, -10) .frame(width: 99) .onAppear{ let list = micList.map({ $0.localizedName }) if !list.contains(micDevice) { micDevice = "default" } } } else { Spacer().frame(width: 6) Picker("", selection: $micDevice) { Text("Default".local).tag("default") ForEach(micList, id: \.self) { device in Text(device.localizedName).tag(device.localizedName) } } .disabled(!recordMic) .padding(.leading, -7.5) .frame(width: 100) .onAppear{ let list = micList.map({ $0.localizedName }) if !list.contains(micDevice) { micDevice = "default" } } } }.padding(.leading, -5) } .buttonStyle(.plain) .scaleEffect(0.69) .padding(.bottom, 4) .frame(width: 110) .background(.primary.opacity(0.00001)) }.frame(height: 80) Divider().frame(height: 70) Button(action: { isPopoverShowing = true }, label: { SelectorView(title: "Mobile Device".local, symbol: "apps.ipad").cornerRadius(8) }).buttonStyle(.plain) .popover(isPresented: $isPopoverShowing, arrowEdge: .bottom) { iDevicePopoverView(closePopover: { isPopoverShowing = false }) } Divider().frame(height: 70) Button(action: { closeMainWindow() appDelegate.openSettingPanel() }, label: { SelectorView(title: "Preferences".local, symbol: "gearshape").cornerRadius(8) }).buttonStyle(.plain) Divider().frame(height: 70) Button(action: { NSApp.terminate(self) }, label: { SelectorView(title: "Quit".local, symbol: "xmark.circle") .cornerRadius(8) .foregroundStyle(.darkMyRed) }).buttonStyle(.plain) } }.padding(10) } } } ================================================ FILE: QuickRecorder/ViewModel/MousePointer.swift ================================================ // // MousePointer.swift // QuickRecorder // // Created by apple on 2024/4/21. // import SwiftUI import Foundation import Cocoa struct MousePointerView: View { @AppStorage("showMouse") private var showMouse: Bool = true var event: NSEvent! var body: some View { ZStack { Circle() .fill(.clear) .overlay( ZStack { Circle() .stroke(style: StrokeStyle(lineWidth: 4)) .foregroundColor(getStrokeColor(event).opacity(0.3)) .padding(4) Circle() .stroke(style: StrokeStyle(lineWidth: 4)) .foregroundColor(getColor(event).opacity(getOpacity(event))) .padding(8) Circle() .stroke(style: StrokeStyle(lineWidth: 1)) .foregroundColor(.gray) .opacity([.rightMouseDown, .rightMouseDragged, .leftMouseDown, .leftMouseDragged, .otherMouseDown, .otherMouseDragged].contains(event.type) ? 0.3 : 0.0) .padding(10) } ) if !showMouse { Circle() .fill(getColor(event).opacity(getOpacity(event))) .frame(width: 8, height: 8) } } } func getOpacity(_ event: NSEvent) -> Double { switch event.type { case .rightMouseDown, .rightMouseDragged, .leftMouseDown, .leftMouseDragged, .otherMouseDown, .otherMouseDragged: return 0.8 default: return 0.3 } } func getColor(_ event: NSEvent) -> Color { switch event.type { case .rightMouseDown, .rightMouseDragged: return .purple case .leftMouseDown, .leftMouseDragged: return .blue case .otherMouseDown, .otherMouseDragged: return .orange default: return .gray } } func getStrokeColor(_ event: NSEvent) -> Color { switch event.type { case .leftMouseUp, .rightMouseUp, .otherMouseUp, .mouseMoved: return .black default: return .clear } } } ================================================ FILE: QuickRecorder/ViewModel/PreviewView.swift ================================================ // // PreviewView.swift // QuickRecorder // // Created by apple on 2024/12/10. // import SwiftUI import Combine struct PreviewView: View { let frame: NSImage let filePath: String private let sharingDelegate = SharingServicePickerDelegate() @State private var isHovered: Bool = false @State private var isHovered2: Bool = false @State private var isSharing: Bool = false @State private var nsWindow: NSWindow? @State private var opacity: Double = 0.0 @AppStorage("trimAfterRecord") private var trimAfterRecord: Bool = false var body: some View { ZStack(alignment: Alignment(horizontal: .leading, vertical: .top)) { ZStack { Color.clear .background(.ultraThickMaterial) .environment(\.controlActiveState, .active) .cornerRadius(6) ZStack { Image(nsImage: frame) .resizable().scaledToFit() .shadow(color: .black.opacity(0.2), radius: 3, y: 1.5) if isHovered2 { Button(action: { if fd.fileExists(atPath: filePath) { NSWorkspace.shared.open(filePath.url) closeWindow() } }, label: { ZStack { Image(systemName: "circle.fill") .font(.system(size: 49)) .foregroundStyle(.black) .opacity(0.5) Image(systemName: "play.circle") .font(.system(size: 50)) .foregroundStyle(.white) .shadow(radius: 4) } }).buttonStyle(.plain) } } .onHover(perform: { isHovered2 = $0 }) .padding(8) } if isHovered { HoverButton(color: .buttonRed, secondaryColor: .buttonRedDark, action: { closeWindow() }, label: { ZStack { Image(systemName: "circle.fill") .font(.title) .foregroundStyle(.white) Image(systemName: "circle.fill") .font(.title2) Image(systemName: "xmark") .font(.system(size: 10, weight: .black)) .foregroundStyle(.white) } }).padding(4) } } .opacity(opacity) .onHover(perform: { isHovered = $0 }) .background(WindowAccessor(onWindowOpen: { w in nsWindow = w })) .onAppear { withAnimation(.easeIn(duration: 0.3)) { opacity = 1.0 } DispatchQueue.main.asyncAfter(deadline: .now() + 6) { if !isHovered && !isSharing { closeWindow() } } } .onChange(of: isHovered) { newValue in if !newValue { DispatchQueue.main.asyncAfter(deadline: .now() + 6) { if !isHovered && !isSharing { closeWindow() } } } } .contextMenu { Button("Show in Finder") { if fd.fileExists(atPath: filePath) { NSWorkspace.shared.activateFileViewerSelecting([filePath.url]) } closeWindow() } Button("Delete") { do { try fd.removeItem(atPath: filePath) } catch { print("Failed to delete file: \(error.localizedDescription)") } closeWindow() } Divider() Button("Copy") { if fd.fileExists(atPath: filePath) { let pasteboard = NSPasteboard.general pasteboard.clearContents() pasteboard.writeObjects([filePath.url as NSURL]) } closeWindow() } Button("共享...") { showSharingServicePicker(for: filePath.url) } Divider() if !trimAfterRecord { Button("Trim") { if fd.fileExists(atPath: filePath) { AppDelegate.shared.createNewWindow(view: VideoTrimmerView(videoURL: filePath.url), title: filePath.lastPathComponent, only: false) } closeWindow() } } if #available(macOS 13, *) { if ["mp4", "mov"].contains(filePath.pathExtension) { Button("Make GIF") { if isAppInstalled(id: "com.sindresorhus.Gifski") { makeGif() closeWindow() } else { let alert = createAlert(title: "Gifski not found", message: "Please install \"Gifski\" first to make GIF!", button1: "Open App Store", button2: "Cancel").runModal() if alert == .alertFirstButtonReturn { openURL("https://apps.apple.com/app/id1351639930") } } } Divider() } } Button("Close") { closeWindow() } } } func openURL(_ urlString: String) { if let url = URL(string: urlString) { NSWorkspace.shared.open(url) } } private func closeWindow() { withAnimation(.easeIn(duration: 0.2)) { opacity = 0.0 } DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { nsWindow?.close() } } private func isAppInstalled(id: String) -> Bool { return NSWorkspace.shared.urlForApplication(withBundleIdentifier: id) != nil } private func makeGif() { let task = Process() task.arguments = ["-b", "com.sindresorhus.Gifski", filePath] task.launchPath = "/usr/bin/open" task.launch() } private func showSharingServicePicker(for url: URL) { if let window = nsWindow { isSharing = true sharingDelegate.onDidChooseService = { service in isSharing = false if service != nil { DispatchQueue.main.async { closeWindow() } } else { DispatchQueue.main.asyncAfter(deadline: .now() + 6) { if !isHovered && !isSharing { closeWindow() } } } } let sharingPicker = NSSharingServicePicker(items: [url]) sharingPicker.delegate = sharingDelegate sharingPicker.show(relativeTo: .zero, of: window.contentView!, preferredEdge: .minY) } } } // 自定义 NSSharingServicePickerDelegate class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate { var onDidChooseService: ((NSSharingService?) -> Void)? func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { onDidChooseService?(service) } } ================================================ FILE: QuickRecorder/ViewModel/QmaPlayer.swift ================================================ // // qmaPlayer.swift // QuickRecorder // // Created by apple on 2024/6/28. // import Foundation import AVFoundation import SwiftUI struct qmaPlayerView: View { @Binding var document: qmaPackageHandle @State var fileURL: URL @State private var overPlay: Bool = false @State private var overStop: Bool = false @State private var overSave: Bool = false @State private var overExport: Bool = false @StateObject private var audioPlayerManager = AudioPlayerManager() var body: some View { ZStack(alignment: .top) { VisualEffectView().ignoresSafeArea() VStack(spacing: 3) { Button {} label: { PlayerSlider(percentage: $audioPlayerManager.progress, audioLength: $audioPlayerManager.audioLength){ editing in if !editing { let newTime = audioPlayerManager.progress * audioPlayerManager.getPlayerDuration() audioPlayerManager.seek(to: newTime) audioPlayerManager.shouldPlay = false } else { if audioPlayerManager.isPlaying { audioPlayerManager.pause() audioPlayerManager.shouldPlay = true } } }.frame(height: 30) } .buttonStyle(.plain) .disabled(audioPlayerManager.exporting) HStack(spacing: 4) { Rectangle().opacity(0.00001).frame(width: 30) Spacer() Button { audioPlayerManager.stop() } label: { ZStack { Rectangle() .cornerRadius(6) .foregroundColor(.secondary.opacity(overStop ? 0.1 : 0.00001)) Image(systemName: "stop.fill") .font(.system(size: 20)) } } .buttonStyle(.plain) .help("Stop Play") .frame(width: 30, height: 30) .disabled(audioPlayerManager.exporting) .onHover { hovering in overStop = hovering } Button { if audioPlayerManager.isPlaying { audioPlayerManager.pause() } else { audioPlayerManager.play() } } label: { ZStack { Rectangle() .cornerRadius(6) .foregroundColor(.secondary.opacity(overPlay ? 0.1 : 0.00001)) Image(systemName: audioPlayerManager.isPlaying ? "pause.fill" : "play.fill") .font(.system(size: 30)) } } .buttonStyle(.plain) .help("Play / Pause") .frame(width: 35, height: 35) .padding(.leading, 2) .disabled(audioPlayerManager.exporting) .onHover { hovering in overPlay = hovering } Button { saveQMA() } label: { ZStack { Rectangle() .cornerRadius(6) .foregroundColor(.secondary.opacity(overSave ? 0.1 : 0.00001)) Image("save") .resizable() .scaledToFit() .frame(width: 16.5) .foregroundColor(.primary) } } .buttonStyle(.plain) .help("Save Changes") .frame(width: 30, height: 30) .disabled(audioPlayerManager.exporting) .onHover { hovering in overSave = hovering } Spacer() Button { saveQMA() audioPlayerManager.export() } label: { ZStack { Rectangle() .cornerRadius(6) .foregroundColor(.secondary.opacity(overExport ? 0.1 : 0.00001)) if audioPlayerManager.exporting { ActivityIndicator() } else { Image(systemName: "square.and.arrow.up") .font(.system(size: 18)) .foregroundColor(.secondary) .offset(y: -2) } } } .buttonStyle(.plain) .help("Export") .frame(width: 30, height: 30) .disabled(audioPlayerManager.exporting) .onHover { hovering in overExport = hovering } } Button {} label: { HStack(spacing: 14) { HStack(spacing: 2) { Image(systemName: "speaker.wave.2.fill") Text("\(Int(audioPlayerManager.sysVol * 100))%").foregroundColor(.secondary).frame(width: 40) VolumeSlider(percentage: $audioPlayerManager.sysVol, maxValue: 4){ editing in } .frame(height: 16) .disabled(audioPlayerManager.exporting) } HStack(spacing: 2) { Image(systemName: "mic.fill") Text("\(Int(audioPlayerManager.micVol * 100))%").foregroundColor(.secondary).frame(width: 40) VolumeSlider(percentage: $audioPlayerManager.micVol, maxValue: 4){ editing in } .frame(height: 16) .disabled(audioPlayerManager.exporting) } } }.buttonStyle(.plain) }.padding().padding(.top, -14) } .onAppear { audioPlayerManager.loadAudioFiles(format: document.info.format, package: fileURL, encoder: document.info.encoder, saveMP3: document.info.exportMP3) audioPlayerManager.sysVol = document.info.sysVol audioPlayerManager.micVol = document.info.micVol } .background(WindowAccessor(onWindowOpen: { w in guard let w = w else { return } w.setContentSize(CGSize(width: 400, height: 100)) w.isMovableByWindowBackground = true w.titlebarAppearsTransparent = true }, onWindowActive: { w in DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { w?.titlebarAppearsTransparent = true } }, onWindowDeactivate: { w in DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { w?.titlebarAppearsTransparent = true } }, onWindowClose: { audioPlayerManager.reset() })) } func saveQMA() { var save = 0 if document.info.sysVol != audioPlayerManager.sysVol { document.info.sysVol = audioPlayerManager.sysVol save += 1 } if document.info.micVol != audioPlayerManager.micVol { document.info.micVol = audioPlayerManager.micVol save += 1 } if save != 0 { NSApp.sendAction(#selector(NSDocument.save(_:)), to: nil, from: nil) } } } struct VisualEffectView: NSViewRepresentable { func makeNSView(context: Context) -> NSVisualEffectView { let effectView = NSVisualEffectView() effectView.state = .active return effectView } func updateNSView(_ nsView: NSVisualEffectView, context: Context) { } } struct VolumeSlider: View { @Binding var percentage: Float @State var maxValue: Float = 1.0 @State private var isDragging = false @State private var isHover = false var onEditingChanged: (Bool) -> Void // Callback for editing changes var body: some View { GeometryReader { geometry in ZStack(alignment: .leading) { Group { Rectangle() .foregroundColor(.secondary.opacity(0.2)) Rectangle() .foregroundColor(.accentColor) .frame(width: geometry.size.width * CGFloat(min(1.0, self.percentage / maxValue))) }.frame(height: 5).cornerRadius(12) Circle() .shadow(radius: 1) .foregroundColor(.white) .opacity(isHover || isDragging ? 1.0 : 0.00001) .frame(width: 16, height: 16) .offset(x: geometry.size.width * CGFloat(min(1.0, self.percentage / maxValue)) - 8) }.onHover { hovering in isHover = hovering } .compositingGroup() .gesture(DragGesture(minimumDistance: 0) .onChanged { value in self.percentage = min(max(0, Float(value.location.x / geometry.size.width) * maxValue), maxValue) self.isDragging = true // Indicate dragging self.onEditingChanged(true) // Notify that editing started } .onEnded { value in // Update the bound percentage value when dragging ends self.percentage = min(max(0, Float(value.location.x / geometry.size.width) * maxValue), maxValue) self.isDragging = false // Indicate dragging ended self.onEditingChanged(false) // Notify that editing ended } ) } } } struct PlayerSlider: View { @Binding var percentage: Double @Binding var audioLength: TimeInterval @State private var isDragging = false @State private var isHover = false @State private var temporaryPercentage: Double = 0.0 // Temporary value during dragging var onEditingChanged: (Bool) -> Void // Callback for editing changes var body: some View { GeometryReader { geometry in VStack(spacing: 2) { HStack { Text("\(String(format: "%.2d:%.2d:%.2d", isDragging ? Int(temporaryPercentage * audioLength) / 3600 : Int(percentage * audioLength) / 3600, isDragging ? Int(temporaryPercentage * audioLength) / 60 : Int(percentage * audioLength) / 60, isDragging ? Int(temporaryPercentage * audioLength) % 60 : Int(percentage * audioLength) % 60))" ).foregroundColor(.secondary) Spacer() Text("\(String(format: "%.2d:%.2d:%.2d", Int(audioLength) / 3600, Int(audioLength) / 60, Int(audioLength) % 60))") .foregroundColor(.secondary) } ZStack(alignment: .leading) { Group { Rectangle() .foregroundColor(.secondary.opacity(0.5)) Rectangle() .foregroundColor(.secondary) .frame(width: geometry.size.width * CGFloat(min(1.0, self.isDragging ? self.temporaryPercentage : self.percentage))) }.frame(height: 4).cornerRadius(12) if isHover || isDragging { Rectangle() .foregroundColor(.black) .blendMode(.destinationOut) .frame(width: 6, height: 10) .offset(x: geometry.size.width * CGFloat(min(1.0, self.isDragging ? self.temporaryPercentage : self.percentage)) - 3) } Rectangle() .cornerRadius(12) .foregroundColor(.primary) .opacity(isHover || isDragging ? 1.0 : 0.00001) .frame(width: 4, height: 12) .offset(x: geometry.size.width * CGFloat(min(1.0, self.isDragging ? self.temporaryPercentage : self.percentage)) - 2) }.onHover { hovering in isHover = hovering } .compositingGroup() .gesture(DragGesture(minimumDistance: 0) .onChanged { value in // Update temporary percentage during dragging self.temporaryPercentage = min(max(0, Double(value.location.x / geometry.size.width)), 1) self.isDragging = true // Indicate dragging self.onEditingChanged(true) // Notify that editing started } .onEnded { value in // Update the bound percentage value when dragging ends self.percentage = self.temporaryPercentage self.isDragging = false // Indicate dragging ended self.onEditingChanged(false) // Notify that editing ended } ) } } } } struct qmaPackageHandle: FileDocument { static var readableContentTypes: [UTType] { [UTType.qma] } var info: Info var sysAudio: Data var micAudio: Data struct Info: Codable { var format: String var encoder: String var exportMP3: Bool var sysVol: Float var micVol: Float } init(info: Info = Info(format: "m4a", encoder: "aac", exportMP3: false, sysVol: 1.0, micVol: 1.0), sysAudio: Data = Data(), micAudio: Data = Data()) { self.info = info self.sysAudio = sysAudio self.micAudio = micAudio } init(configuration: ReadConfiguration) throws { guard let fileWrappers = configuration.file.fileWrappers else { throw CocoaError(.fileReadCorruptFile) } guard let infoFileWrapper = fileWrappers["info.json"], let infoData = infoFileWrapper.regularFileContents, let info = try? JSONDecoder().decode(Info.self, from: infoData) else { throw CocoaError(.fileReadCorruptFile) } self.info = info guard let sysAudioFileWrapper = fileWrappers["sys.\(info.format)"], let sysAudio = sysAudioFileWrapper.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } self.sysAudio = sysAudio guard let micAudioFileWrapper = fileWrappers["mic.\(info.format)"], let micAudio = micAudioFileWrapper.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } self.micAudio = micAudio } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { let infoData = try JSONEncoder().encode(info) let infoFileWrapper = FileWrapper(regularFileWithContents: infoData) infoFileWrapper.preferredFilename = "info.json" let sysAudioFileWrapper = FileWrapper(regularFileWithContents: sysAudio) sysAudioFileWrapper.preferredFilename = "sys.\(info.format)" let micAudioFileWrapper = FileWrapper(regularFileWithContents: micAudio) micAudioFileWrapper.preferredFilename = "mic.\(info.format)" let fileWrapper = FileWrapper(directoryWithFileWrappers: [ "info.json": infoFileWrapper, "sys.\(info.format)": sysAudioFileWrapper, "mic.\(info.format)": micAudioFileWrapper ]) return fileWrapper } } extension qmaPackageHandle { static func load(from url: URL) throws -> qmaPackageHandle { let fileWrapper = try FileWrapper(url: url, options: .immediate) guard let infoFileWrapper = fileWrapper.fileWrappers?["info.json"], let infoData = infoFileWrapper.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } let info = try JSONDecoder().decode(Info.self, from: infoData) guard let sysAudioFileWrapper = fileWrapper.fileWrappers?["sys.\(info.format)"], let sysAudio = sysAudioFileWrapper.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } guard let micAudioFileWrapper = fileWrapper.fileWrappers?["mic.\(info.format)"], let micAudio = micAudioFileWrapper.regularFileContents else { throw CocoaError(.fileReadCorruptFile) } return qmaPackageHandle(info: info, sysAudio: sysAudio, micAudio: micAudio) } } class AudioPlayerManager: ObservableObject { @Published var progress: Double = 0.0 @Published var isPlaying: Bool = false @Published var shouldPlay: Bool = false @Published var exporting: Bool = false @Published var audioLength: TimeInterval = 0 @Published var sysVol: Float = 1.0 { didSet { updateSysVol() } } @Published var micVol: Float = 1.0 { didSet { updateMicVol() } } private var engine = AVAudioEngine() private var playerNode1 = AVAudioPlayerNode() private var playerNode2 = AVAudioPlayerNode() private var mixerNode = AVAudioMixerNode() private var timer: Timer? private var lastStartFramePosition = AVAudioFramePosition(0.0) private var audioFile1: AVAudioFile? private var audioFile2: AVAudioFile? private var isSeeking = false private var exportMP3 = false private var fileFormat = "m4a" private var fileEncoder = "aac" private var packageURL: URL? private var seekTime: Double = 0 private var panel = NSSavePanel() init() { setupAudioEngine() } private func setupAudioEngine() { engine.attach(playerNode1) engine.attach(playerNode2) engine.attach(mixerNode) let outputFormat = AVAudioFormat(commonFormat: .pcmFormatFloat32, sampleRate: 48000, channels: 2, interleaved: false)! engine.connect(playerNode1, to: mixerNode, format: outputFormat) engine.connect(playerNode2, to: mixerNode, format: outputFormat) engine.connect(mixerNode, to: engine.mainMixerNode, format: outputFormat) do { try engine.start() } catch { print("Audio engine start error: \(error)") } } func loadAudioFiles(format: String, package: URL, encoder: String, saveMP3: Bool) { do { fileFormat = format fileEncoder = encoder exportMP3 = saveMP3 packageURL = package audioFile1 = try AVAudioFile(forReading: package.appendingPathComponent("sys.\(format)")) audioFile2 = try AVAudioFile(forReading: package.appendingPathComponent("mic.\(format)")) audioLength = Double(audioFile1?.length ?? 0) / (audioFile1?.processingFormat.sampleRate ?? 48000.0) updateSysVol() updateMicVol() } catch { print("Error loading audio data: \(error)") } } func play() { guard let audioFile1 = audioFile1, let audioFile2 = audioFile2 else { return } playerNode1.scheduleFile(audioFile1, at: nil, completionHandler: nil) playerNode2.scheduleFile(audioFile2, at: nil, completionHandler: nil) playerNode1.play() playerNode2.play() stopProgressTimer() startProgressTimer() isPlaying = true } func pause() { playerNode1.pause() playerNode2.pause() stopProgressTimer() isPlaying = false } func stop() { playerNode1.stop() playerNode2.stop() stopProgressTimer() lastStartFramePosition = AVAudioFramePosition(0.0) progress = 0.0 isPlaying = false } func seek(to time: Double) { guard let audioFile1 = audioFile1, let audioFile2 = audioFile2 else { return } playerNode1.stop() playerNode2.stop() stopProgressTimer() seekTime = time isSeeking = true let startFrame = AVAudioFramePosition(seekTime * audioFile1.processingFormat.sampleRate) let frameCount = AVAudioFrameCount(audioFile1.length - startFrame) if frameCount > 0 { lastStartFramePosition = startFrame playerNode1.scheduleSegment(audioFile1, startingFrame: startFrame, frameCount: frameCount, at: nil, completionHandler: nil) playerNode2.scheduleSegment(audioFile2, startingFrame: startFrame, frameCount: frameCount, at: nil, completionHandler: nil) progress = time / audioLength if isPlaying || shouldPlay { playerNode1.play() playerNode2.play() startProgressTimer() isPlaying = true } } else { stop() } } private func startProgressTimer() { timer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: true) { _ in if let lastRenderTime = self.playerNode1.lastRenderTime, let playerTime = self.playerNode1.playerTime(forNodeTime: lastRenderTime) { let currentTime = Double(self.lastStartFramePosition + playerTime.sampleTime) / playerTime.sampleRate self.progress = currentTime / self.audioLength if currentTime > self.audioLength { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { self.stop() } } } } } private func stopProgressTimer() { timer?.invalidate() timer = nil } func getPlayerDuration() -> TimeInterval { return audioLength } func reset() { stop() playerNode1.reset() playerNode2.reset() audioFile1 = nil audioFile2 = nil } func export() { guard let packageURL = packageURL else { return } stop() let format = exportMP3 ? "mp3" : self.fileFormat showSavePanel(defaultFileName: "\(packageURL.deletingPathExtension().appendingPathExtension(format).lastPathComponent)", exportMP3: exportMP3) { url, saveAsMP3 in if let url = url { self.saveFile(url, saveAsMP3: saveAsMP3) } } } func saveFile(_ url: URL, saveAsMP3: Bool = false) { var url = url if url.pathExtension == "mp3" { url = url.deletingPathExtension() } if url.pathExtension != self.fileFormat { url = url.appendingPathExtension(self.fileFormat) } let lastComp = url.lastPathComponent if self.exportMP3 { url = url.deletingLastPathComponent().appendingPathComponent("." + url.lastPathComponent) } Thread.detachNewThread { DispatchQueue.main.async { self.exporting = true } do { guard let audioFile1 = self.audioFile1, let audioFile2 = self.audioFile2 else { return } self.playerNode1.scheduleFile(audioFile1, at: nil, completionHandler: nil) self.playerNode2.scheduleFile(audioFile2, at: nil, completionHandler: nil) let audioSettings = SCContext.updateAudioSettings(format: self.fileEncoder) let outputFormat = self.playerNode1.outputFormat(forBus: 0) let outputFile = try AVAudioFile(forWriting: url, settings: audioSettings, commonFormat: .pcmFormatFloat32, interleaved: false) self.engine.stop() try self.engine.enableManualRenderingMode(.offline, format: outputFormat, maximumFrameCount: 4096) try self.engine.start() self.playerNode1.play() self.playerNode2.play() let duration = audioFile1.length let buffer = AVAudioPCMBuffer(pcmFormat: self.engine.manualRenderingFormat, frameCapacity: self.engine.manualRenderingMaximumFrameCount)! while self.engine.manualRenderingSampleTime < duration { let framesToRender = min(UInt32(buffer.frameCapacity), UInt32(duration - self.engine.manualRenderingSampleTime)) let status = try self.engine.renderOffline(framesToRender, to: buffer) switch status { case .success: try outputFile.write(from: buffer) case .insufficientDataFromInputNode, .cannotDoInCurrentContext: // Handle the cases where rendering cannot proceed break default: // Handle other cases if needed break } } self.engine.disableManualRenderingMode() self.engine.stop() self.setupAudioEngine() let title = "Recording Completed".local var body = String(format: "File saved to: %@".local, url.path.removingPercentEncoding!) let id = "quickrecorder.completed.\(UUID().uuidString)" if saveAsMP3 { let oldURL = url let newURl = url.deletingLastPathComponent().appendingPathComponent(lastComp).deletingPathExtension().appendingPathExtension("mp3") body = String(format: "File saved to: %@".local, newURl.path.removingPercentEncoding!) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { Task { do { try await SCContext.m4a2mp3(inputUrl: oldURL, outputUrl: newURl) try? fd.removeItem(at: oldURL) } catch { SCContext.showNotification(title: "Failed to save file".local, body: "\(error.localizedDescription)", id: "quickrecorder.error.\(UUID().uuidString)") return } } } } SCContext.showNotification(title: title, body: body, id: id) } catch { SCContext.showNotification(title: "Failed to save file".local, body: "\(error.localizedDescription)", id: "quickrecorder.error.\(UUID().uuidString)") } DispatchQueue.main.async { self.exporting = false } } } private func updateSysVol() { playerNode1.volume = sysVol } private func updateMicVol() { playerNode2.volume = micVol } private func showSavePanel(defaultFileName: String, exportMP3: Bool, completion: @escaping (URL?, Bool) -> Void) { panel.isReleasedWhenClosed = true panel.nameFieldStringValue = defaultFileName panel.canCreateDirectories = true panel.title = "Export Recording".local let checkBox = NSButton(checkboxWithTitle: "Export as MP3".local, target: self, action: #selector(checkBoxToggled(_:))) checkBox.state = exportMP3 ? .on : .off let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: checkBox.frame.width, height: checkBox.frame.height)) accessoryView.addSubview(checkBox) panel.accessoryView = accessoryView panel.begin { response in if response == .OK { let exportAsMP3 = (checkBox.state == .on) completion(self.panel.url, exportAsMP3) } else { completion(nil, false) } } } @objc private func checkBoxToggled(_ sender: NSButton) { panel.close() panel = NSSavePanel() exportMP3.toggle() export() } } extension UTType { static let qma = UTType(exportedAs: "com.lihaoyun6.QuickRecorder.qma") } ================================================ FILE: QuickRecorder/ViewModel/ScreenMagnifier.swift ================================================ // // ScreenMagnifier.swift // QuickRecorder // // Created by apple on 2024/4/25. // import SwiftUI struct ScreenMagnifier: View { @State var screenShot: NSImage! @State var scaleFactor = SCContext.getScreenWithMouse()?.backingScaleFactor ?? 1.0 var event: NSEvent! var body: some View { ZStack { Rectangle() .fill(Color.clear) .overlay( Rectangle() .stroke(style: StrokeStyle(lineWidth: 2)) .padding(1) .foregroundColor(.blue.opacity(0.5)) ) .background( Image(nsImage: screenShot!) .interpolation(.none) .resizable() .scaledToFit() .frame(width: (screenShot?.size.width)!*3, height: (screenShot?.size.height)!*3) ) } } func getOpacity(_ event: NSEvent) -> Double { switch event.type { case .rightMouseDown, .rightMouseDragged, .leftMouseDown, .leftMouseDragged, .otherMouseDown, .otherMouseDragged: return 0.8 default: return 0.3 } } func getColor(_ event: NSEvent) -> Color { switch event.type { case .rightMouseDown, .rightMouseDragged: return .purple case .leftMouseDown, .leftMouseDragged: return .blue case .otherMouseDown, .otherMouseDragged: return .orange default: return .gray } } func getStrokeColor(_ event: NSEvent) -> Color { switch event.type { case .leftMouseUp, .rightMouseUp, .otherMouseUp, .mouseMoved: return .black default: return .clear } } } ================================================ FILE: QuickRecorder/ViewModel/ScreenSelector.swift ================================================ // // ScreenSelector.swift // QuickRecorder // // Created by apple on 2024/4/18. // import SwiftUI import ScreenCaptureKit struct ScreenSelector: View { @Environment(\.colorScheme) var colorScheme @StateObject var viewModel = ScreenSelectorViewModel() @State private var selected: SCDisplay? @State private var isPopoverShowing = false @State private var autoStop = 0 var appDelegate = AppDelegate.shared var body: some View { ZStack { VStack(spacing: 15) { Text("Please select the screen to record") let count = viewModel.screenThumbnails.count ScrollView(.vertical) { VStack(spacing: 14){ ForEach(0..